diff --git a/moonbeam-types-bundle/index.ts b/moonbeam-types-bundle/index.ts index aef584c169..1cc408059c 100644 --- a/moonbeam-types-bundle/index.ts +++ b/moonbeam-types-bundle/index.ts @@ -78,12 +78,17 @@ export const moonbeamDefinitions = { ExtrinsicSignature: "EthereumSignature", RoundIndex: "u32", Candidate: { - validator: "AccountId", + id: "AccountId", fee: "Perbill", + bond: "Balance", nominators: "Vec", total: "Balance", state: "ValidatorStatus", }, + Nominator: { + nominations: "Vec", + total: "Balance", + }, Bond: { owner: "AccountId", amount: "Balance", diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index df39e52ded..9b2be89bee 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -16,8 +16,8 @@ use cumulus_primitives::ParaId; use moonbeam_runtime::{ - AccountId, BalancesConfig, EVMConfig, EthereumChainIdConfig, EthereumConfig, GenesisConfig, - ParachainInfoConfig, StakeConfig, SudoConfig, SystemConfig, GLMR, WASM_BINARY, + AccountId, Balance, BalancesConfig, EVMConfig, EthereumChainIdConfig, EthereumConfig, + GenesisConfig, ParachainInfoConfig, StakeConfig, SudoConfig, SystemConfig, GLMR, WASM_BINARY, }; use sc_chain_spec::{ChainSpecExtension, ChainSpecGroup}; use sc_service::ChainType; @@ -54,6 +54,12 @@ pub fn development_chain_spec() -> ChainSpec { move || { testnet_genesis( AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + // Validator + vec![( + AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + None, + 100_000 * GLMR, + )], vec![AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap()], Default::default(), // para_id 1281, //ChainId @@ -80,6 +86,12 @@ pub fn get_chain_spec(para_id: ParaId) -> ChainSpec { move || { testnet_genesis( AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + // Validator + vec![( + AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap(), + None, + 100_000 * GLMR, + )], vec![AccountId::from_str("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b").unwrap()], para_id, 1280, //ChainId @@ -98,6 +110,7 @@ pub fn get_chain_spec(para_id: ParaId) -> ChainSpec { fn testnet_genesis( root_key: AccountId, + stakers: Vec<(AccountId, Option, Balance)>, endowed_accounts: Vec, para_id: ParaId, chain_id: u64, @@ -125,12 +138,6 @@ fn testnet_genesis( accounts: BTreeMap::new(), }), pallet_ethereum: Some(EthereumConfig {}), - stake: Some(StakeConfig { - stakers: endowed_accounts - .iter() - .cloned() - .map(|k| (k, None, 100_000 * GLMR)) - .collect(), - }), + stake: Some(StakeConfig { stakers }), } } diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index de2e8372ca..acdebd1a5c 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -38,9 +38,9 @@ //! stored in the `ExitQueue` and processed `BondDuration` rounds later to unstake the validator //! and all of its nominators. //! -//! To join the set of nominators, an account must not be a validator candidate nor an existing -//! nominator. To join the set of nominators, an account must call `join_nominators` with -//! stake >= `MinNominatorStk`. +//! To join the set of nominators, an account must call `join_nominators` with +//! stake >= `MinNominatorStk`. There are also runtime methods for nominating additional validators +//! and revoking nominations. #![recursion_limit = "256"] #![cfg_attr(not(feature = "std"), no_std)] @@ -111,46 +111,43 @@ impl Into> for Bond { #[derive(Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] /// The activity status of the validator -pub enum ValidatorStatus { +pub enum ValidatorStatus { /// Committed to be online and producing valid blocks (not equivocating) Active, /// Temporarily inactive and excused for inactivity Idle, - /// Bonded until the wrapped block - Leaving(BlockNumber), + /// Bonded until the inner round + Leaving(RoundIndex), } -impl Default for ValidatorStatus { - fn default() -> ValidatorStatus { +impl Default for ValidatorStatus { + fn default() -> ValidatorStatus { ValidatorStatus::Active } } #[derive(Encode, Decode, RuntimeDebug)] -pub struct CandidateState { - pub validator: AccountId, +pub struct Validator { + pub id: AccountId, pub fee: Perbill, + pub bond: Balance, pub nominators: OrderedSet>, pub total: Balance, - pub state: ValidatorStatus, + pub state: ValidatorStatus, } impl< A: Ord + Clone, B: AtLeast32BitUnsigned + Ord + Copy + sp_std::ops::AddAssign + sp_std::ops::SubAssign, - C: Ord + Copy, - > CandidateState + > Validator { - pub fn new(validator: A, fee: Perbill, bond: B) -> Self { - let nominators = OrderedSet::from(vec![Bond { - owner: validator.clone(), - amount: bond, - }]); + pub fn new(id: A, fee: Perbill, bond: B) -> Self { let total = bond; - CandidateState { - validator, + Validator { + id, fee, - nominators, + bond, + nominators: OrderedSet::new(), total, state: ValidatorStatus::default(), // default active } @@ -159,10 +156,88 @@ impl< self.state == ValidatorStatus::Active } pub fn is_leaving(&self) -> bool { - if let ValidatorStatus::Leaving(_) = self.state { - true + matches!(self.state, ValidatorStatus::Leaving(_)) + } + pub fn bond_more(&mut self, more: B) { + self.bond += more; + self.total += more; + } + // Returns None if underflow or less == self.bond (in which case validator should leave instead) + pub fn bond_less(&mut self, less: B) -> Option { + if self.bond > less { + self.bond -= less; + self.total -= less; + Some(self.bond) } else { - false + None + } + } + // infallible so nominator must exist before calling + pub fn rm_nominator(&mut self, nominator: A) -> B { + let mut total = self.total; + let nominators = self + .nominators + .0 + .iter() + .filter_map(|x| { + if x.owner == nominator { + total -= x.amount; + None + } else { + Some(x.clone()) + } + }) + .collect(); + self.nominators = OrderedSet::from(nominators); + self.total = total; + total + } + // infallible so nominator dne before calling + pub fn add_nominator(&mut self, owner: A, amount: B) -> B { + self.nominators.insert(Bond { owner, amount }); + self.total += amount; + self.total + } + // only call with an amount larger than existing amount + pub fn update_nominator(&mut self, nominator: A, amount: B) -> B { + let mut difference: B = 0u32.into(); + let nominators = self + .nominators + .0 + .iter() + .map(|x| { + if x.owner == nominator { + // new amount must be greater or will underflow + difference = amount - x.amount; + Bond { + owner: x.owner.clone(), + amount, + } + } else { + x.clone() + } + }) + .collect(); + self.nominators = OrderedSet::from(nominators); + self.total += difference; + self.total + } + pub fn inc_nominator(&mut self, nominator: A, more: B) { + for x in &mut self.nominators.0 { + if x.owner == nominator { + x.amount += more; + self.total += more; + return; + } + } + } + pub fn dec_nominator(&mut self, nominator: A, less: B) { + for x in &mut self.nominators.0 { + if x.owner == nominator { + x.amount -= less; + self.total -= less; + return; + } } } pub fn go_offline(&mut self) { @@ -171,34 +246,176 @@ impl< pub fn go_online(&mut self) { self.state = ValidatorStatus::Active; } - pub fn leave_candidates(&mut self, block: C) { - self.state = ValidatorStatus::Leaving(block); + pub fn leave_candidates(&mut self, round: RoundIndex) { + self.state = ValidatorStatus::Leaving(round); } } -impl Into> for CandidateState { +impl Into> for Validator { fn into(self) -> Exposure { - let mut others = Vec::>::new(); - let mut own = Zero::zero(); - for Bond { owner, amount } in self.nominators.0 { - if owner == self.validator { - own = amount; + Exposure { + total: self.total, + own: self.bond, + others: self.nominators.0.into_iter().map(|x| x.into()).collect(), + } + } +} + +#[derive(Encode, Decode, RuntimeDebug)] +pub struct Nominator { + pub nominations: OrderedSet>, + pub total: Balance, +} + +impl< + AccountId: Ord + Clone, + Balance: Copy + + sp_std::ops::AddAssign + + sp_std::ops::Add + + sp_std::ops::SubAssign + + PartialOrd, + > Nominator +{ + pub fn new(validator: AccountId, nomination: Balance) -> Self { + Nominator { + nominations: OrderedSet::from(vec![Bond { + owner: validator, + amount: nomination, + }]), + total: nomination, + } + } + pub fn add_nomination(&mut self, bond: Bond) -> bool { + let amt = bond.amount; + if self.nominations.insert(bond) { + self.total += amt; + true + } else { + false + } + } + // Returns Some(remaining balance), must be more than MinNominatorStk + // Returns None if nomination not found + pub fn rm_nomination(&mut self, validator: AccountId) -> Option { + let mut amt: Option = None; + let nominations = self + .nominations + .0 + .iter() + .filter_map(|x| { + if x.owner == validator { + amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + if let Some(balance) = amt { + self.nominations = OrderedSet::from(nominations); + self.total -= balance; + Some(self.total) + } else { + None + } + } + // Returns Some(new balances) if old was nominated and None if it wasn't nominated + pub fn swap_nomination( + &mut self, + old: AccountId, + new: AccountId, + ) -> Option<(Balance, Balance)> { + let mut amt: Option = None; + let nominations = self + .nominations + .0 + .iter() + .filter_map(|x| { + if x.owner == old { + amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + if let Some(swapped_amt) = amt { + let mut old_new_amt: Option = None; + let nominations2 = self + .nominations + .0 + .iter() + .filter_map(|x| { + if x.owner == new { + old_new_amt = Some(x.amount); + None + } else { + Some(x.clone()) + } + }) + .collect(); + let new_amount = if let Some(old_amt) = old_new_amt { + // update existing nomination + self.nominations = OrderedSet::from(nominations2); + let new_amt = old_amt + swapped_amt; + self.nominations.insert(Bond { + owner: new, + amount: new_amt, + }); + new_amt } else { - others.push(Bond { owner, amount }.into()); + // insert completely new nomination + self.nominations = OrderedSet::from(nominations); + self.nominations.insert(Bond { + owner: new, + amount: swapped_amt, + }); + swapped_amt + }; + Some((swapped_amt, new_amount)) + } else { + None + } + } + // Returns None if nomination not found + pub fn inc_nomination(&mut self, validator: AccountId, more: Balance) -> Option { + for x in &mut self.nominations.0 { + if x.owner == validator { + x.amount += more; + self.total += more; + return Some(x.amount); } } - Exposure { - total: self.total, - own, - others, + None + } + // Returns Some(Some(balance)) if successful + // None if nomination not found + // Some(None) if underflow + pub fn dec_nomination( + &mut self, + validator: AccountId, + less: Balance, + ) -> Option> { + for x in &mut self.nominations.0 { + if x.owner == validator { + if x.amount > less { + x.amount -= less; + self.total -= less; + return Some(Some(x.amount)); + } else { + // underflow error; should rm entire nomination if x.amount == validator + return Some(None); + } + } } + None } } type RoundIndex = u32; type RewardPoint = u32; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -type Candidate = CandidateState<::AccountId, BalanceOf, RoundIndex>; +type Candidate = Validator<::AccountId, BalanceOf>; pub trait Config: System { /// The overarching event type @@ -213,12 +430,16 @@ pub trait Config: System { type MaxValidators: Get; /// Maximum nominators per validator type MaxNominatorsPerValidator: Get; + /// Maximum validators per nominator + type MaxValidatorsPerNominator: Get; /// Balance issued as rewards per round (constant issuance) type IssuancePerRound: Get>; /// Maximum fee for any validator type MaxFee: Get; /// Minimum stake for any registered on-chain account to become a validator type MinValidatorStk: Get>; + /// Minimum stake for any registered on-chain account to nominate + type MinNomination: Get>; /// Minimum stake for any registered on-chain account to become a nominator type MinNominatorStk: Get>; } @@ -236,16 +457,30 @@ decl_event!( JoinedValidatorCandidates(AccountId, Balance, Balance), /// Round, Validator Account, Total Exposed Amount (includes all nominations) ValidatorChosen(RoundIndex, AccountId, Balance), + /// Validator Account, Old Bond, New Bond + ValidatorBondedMore(AccountId, Balance, Balance), + /// Validator Account, Old Bond, New Bond + ValidatorBondedLess(AccountId, Balance, Balance), ValidatorWentOffline(RoundIndex, AccountId), ValidatorBackOnline(RoundIndex, AccountId), /// Round, Validator Account, Scheduled Exit ValidatorScheduledExit(RoundIndex, AccountId, RoundIndex), /// Account, Amount Unlocked, New Total Amt Locked ValidatorLeft(AccountId, Balance, Balance), - /// Nominator, Validator, Amount Unstaked, New Total Amt Staked for Validator - NominatorLeft(AccountId, AccountId, Balance, Balance), + // Nominator, Validator, Old Nomination, New Nomination + NominationIncreased(AccountId, AccountId, Balance, Balance), + // Nominator, Validator, Old Nomination, New Nomination + NominationDecreased(AccountId, AccountId, Balance, Balance), + // Nominator, Swapped Amount, Old Nominator, New Nominator + NominationSwapped(AccountId, Balance, AccountId, AccountId), + /// Nominator, Amount Staked + NominatorJoined(AccountId, Balance), + /// Nominator, Amount Unstaked + NominatorLeft(AccountId, Balance), /// Nominator, Amount Locked, Validator, New Total Amt Locked ValidatorNominated(AccountId, Balance, AccountId, Balance), + /// Nominator, Validator, Amount Unstaked, New Total Amt Staked for Validator + NominatorLeftValidator(AccountId, AccountId, Balance, Balance), Rewarded(AccountId, Balance), } ); @@ -255,18 +490,22 @@ decl_error! { // Nominator Does Not Exist NominatorDNE, CandidateDNE, - ValidatorDNE, NominatorExists, CandidateExists, - ValidatorExists, FeeOverMax, ValBondBelowMin, NomBondBelowMin, + NominationBelowMin, AlreadyOffline, AlreadyActive, AlreadyLeaving, TooManyNominators, CannotActivateIfLeaving, + ExceedMaxValidatorsPerNom, + AlreadyNominatedValidator, + NominationDNE, + Underflow, + CannotSwitchToSameNomination, } } @@ -275,7 +514,8 @@ decl_storage! { /// Current round, incremented every `BlocksPerRound` in `fn on_finalize` Round: RoundIndex; /// Current nominators with their validator - Nominators: map hasher(blake2_128_concat) T::AccountId => Option; + Nominators: map + hasher(blake2_128_concat) T::AccountId => Option>>; /// Current candidates with associated state Candidates: map hasher(blake2_128_concat) T::AccountId => Option>; /// Current validator set @@ -308,7 +548,7 @@ decl_storage! { "Stash does not have enough balance to bond." ); let _ = if let Some(nominated_val) = opt_val { - >::nominate( + >::join_nominators( T::Origin::from(Some(actor.clone()).into()), nominated_val.clone(), balance, @@ -316,7 +556,7 @@ decl_storage! { } else { >::join_candidates( T::Origin::from(Some(actor.clone()).into()), - Perbill::from_percent(2),// default fee for validators set at genesis is 2% + Perbill::zero(), // default fee for validators registered at genesis is 0% balance, ) }; @@ -352,13 +592,35 @@ decl_module! { candidates.insert(Bond{owner: acc.clone(), amount: bond}), Error::::CandidateExists ); - T::Currency::reserve(&acc,bond)?; - let candidate: Candidate = CandidateState::new(acc.clone(),fee,bond); + T::Currency::reserve(&acc, bond)?; + let candidate: Candidate = Validator::new(acc.clone(), fee, bond); let new_total = >::get() + bond; >::put(new_total); - >::insert(&acc,candidate); + >::insert(&acc, candidate); >::put(candidates); - Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc,bond,new_total)); + Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc, bond, new_total)); + Ok(()) + } + #[weight = 0] + fn leave_candidates(origin) -> DispatchResult { + let validator = ensure_signed(origin)?; + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + ensure!(!state.is_leaving(),Error::::AlreadyLeaving); + let mut exits = >::get(); + let now = ::get(); + let when = now + T::BondDuration::get(); + ensure!( + exits.insert(Bond{owner:validator.clone(),amount:when}), + Error::::AlreadyLeaving + ); + state.leave_candidates(when); + let mut candidates = >::get(); + if candidates.remove(&Bond::from_owner(validator.clone())) { + >::put(candidates); + } + >::put(exits); + >::insert(&validator,state); + Self::deposit_event(RawEvent::ValidatorScheduledExit(now,validator,when)); Ok(()) } #[weight = 0] @@ -393,90 +655,188 @@ decl_module! { Ok(()) } #[weight = 0] - fn leave_candidates(origin) -> DispatchResult { + fn candidate_bond_more(origin, more: BalanceOf) -> DispatchResult { let validator = ensure_signed(origin)?; let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; - ensure!(!state.is_leaving(),Error::::AlreadyLeaving); - let mut exits = >::get(); - let now = ::get(); - let when = now + T::BondDuration::get(); - ensure!( - exits.insert(Bond{owner:validator.clone(),amount:when}), - Error::::AlreadyLeaving - ); - state.leave_candidates(when); - let mut candidates = >::get(); - if candidates.remove(&Bond::from_owner(validator.clone())) { - >::put(candidates); + ensure!(!state.is_leaving(),Error::::CannotActivateIfLeaving); + T::Currency::reserve(&validator, more)?; + let before = state.bond; + state.bond_more(more); + let after = state.bond; + if state.is_active() { + Self::update_active(validator.clone(), state.total); } - >::put(exits); >::insert(&validator,state); - Self::deposit_event(RawEvent::ValidatorScheduledExit(now,validator,when)); + Self::deposit_event(RawEvent::ValidatorBondedMore(validator, before, after)); + Ok(()) + } + #[weight = 0] + fn candidate_bond_less(origin, less: BalanceOf) -> DispatchResult { + let validator = ensure_signed(origin)?; + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + ensure!(!state.is_leaving(),Error::::CannotActivateIfLeaving); + let before = state.bond; + let after = state.bond_less(less).ok_or(Error::::Underflow)?; + ensure!(after >= T::MinValidatorStk::get(), Error::::ValBondBelowMin); + T::Currency::unreserve(&validator, less); + if state.is_active() { + Self::update_active(validator.clone(), state.total); + } + >::insert(&validator, state); + Self::deposit_event(RawEvent::ValidatorBondedLess(validator, before, after)); Ok(()) } #[weight = 0] - fn nominate( + fn join_nominators( origin, validator: T::AccountId, amount: BalanceOf, ) -> DispatchResult { let acc = ensure_signed(origin)?; + ensure!(amount >= T::MinNominatorStk::get(), Error::::NomBondBelowMin); ensure!(!Self::is_nominator(&acc),Error::::NominatorExists); ensure!(!Self::is_candidate(&acc),Error::::CandidateExists); + Self::nominator_joins_validator(acc.clone(), amount, validator.clone())?; + >::insert(&acc, Nominator::new(validator, amount)); + Self::deposit_event(RawEvent::NominatorJoined(acc, amount)); + Ok(()) + } + #[weight = 0] + fn leave_nominators(origin) -> DispatchResult { + let acc = ensure_signed(origin)?; + let nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + for bond in nominator.nominations.0 { + Self::nominator_leaves_validator(acc.clone(), bond.owner.clone())?; + } + >::remove(&acc); + Self::deposit_event(RawEvent::NominatorLeft(acc, nominator.total)); + Ok(()) + } + #[weight = 0] + fn nominate_new( + origin, + validator: T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + let acc = ensure_signed(origin)?; + ensure!(amount >= T::MinNomination::get(), Error::::NominationBelowMin); + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + ensure!( + nominator.nominations.0.len() < T::MaxValidatorsPerNominator::get(), + Error::::ExceedMaxValidatorsPerNom + ); let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; - ensure!(amount >= T::MinNominatorStk::get(), Error::::NomBondBelowMin); + ensure!( + nominator.add_nomination(Bond{owner:validator.clone(), amount}), + Error::::AlreadyNominatedValidator + ); let nomination = Bond { owner: acc.clone(), amount, }; - ensure!(state.nominators.insert(nomination),Error::::NominatorExists); ensure!( - state.nominators.0.len() <= T::MaxNominatorsPerValidator::get(), + state.nominators.0.len() < T::MaxNominatorsPerValidator::get(), Error::::TooManyNominators ); - T::Currency::reserve(&acc,amount)?; + 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_candidate(validator.clone(),new_total); + Self::update_active(validator.clone(), new_total); } let new_total_locked = >::get() + amount; >::put(new_total_locked); - >::insert(&acc,validator.clone()); state.total = new_total; - >::insert(&validator,state); - Self::deposit_event(RawEvent::ValidatorNominated(acc,amount,validator,new_total)); + >::insert(&validator, state); + >::insert(&acc, nominator); + Self::deposit_event(RawEvent::ValidatorNominated( + acc, amount, validator, new_total, + )); Ok(()) } #[weight = 0] - fn leave_nominators(origin) -> DispatchResult { + fn switch_nomination(origin, old: T::AccountId, new: T::AccountId) -> DispatchResult { + let acc = ensure_signed(origin)?; + ensure!(old != new, Error::::CannotSwitchToSameNomination); + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + let mut old_validator = >::get(&old).ok_or(Error::::CandidateDNE)?; + let mut new_validator = >::get(&new).ok_or(Error::::CandidateDNE)?; + let (swapped_amt, new_amt) = nominator + .swap_nomination(old.clone(), new.clone()) + .ok_or(Error::::NominationDNE)?; + let (new_old, new_new) = if new_amt > swapped_amt { + (old_validator.rm_nominator(acc.clone()), new_validator.update_nominator(acc.clone(), new_amt)) + } else { + (old_validator.rm_nominator(acc.clone()), new_validator.add_nominator(acc.clone(), swapped_amt)) + }; + if old_validator.is_active() { + Self::update_active(old.clone(), new_old); + } + if new_validator.is_active() { + Self::update_active(new.clone(), new_new); + } + >::insert(&old, old_validator); + >::insert(&new, new_validator); + >::insert(&acc, nominator); + Self::deposit_event(RawEvent::NominationSwapped(acc, swapped_amt, old, new)); + Ok(()) + } + #[weight = 0] + fn revoke_nomination(origin, validator: T::AccountId) -> DispatchResult { + Self::nominator_revokes_validator(ensure_signed(origin)?, validator) + } + #[weight = 0] + fn nominator_bond_more( + origin, + candidate: T::AccountId, + more: BalanceOf + ) -> DispatchResult { let nominator = ensure_signed(origin)?; - let validator = >::get(&nominator).ok_or(Error::::NominatorDNE)?; - let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; - let mut exists: Option> = None; - let noms = state.nominators.0.into_iter().filter_map(|nom| { - if nom.owner != nominator { - Some(nom) - } else { - exists = Some(nom.amount); - None - } - }).collect(); - let nominators = OrderedSet::from(noms); - let nominator_stake = exists.ok_or(Error::::NominatorDNE)?; - T::Currency::unreserve(&nominator,nominator_stake); - state.nominators = nominators; - let new_total = state.total - nominator_stake; - if state.is_active() { - Self::update_active_candidate(validator.clone(),new_total); + let mut nominations = >::get(&nominator).ok_or(Error::::NominatorDNE)?; + let mut validator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + let _ = nominations + .inc_nomination(candidate.clone(), more) + .ok_or(Error::::NominationDNE)?; + T::Currency::reserve(&nominator, more)?; + let before = validator.total; + validator.inc_nominator(nominator.clone(), more); + let after = validator.total; + if validator.is_active() { + Self::update_active(candidate.clone(), validator.total); } - state.total = new_total; - let new_total_locked = >::get() - nominator_stake; - >::put(new_total_locked); - >::insert(&validator,state); - >::remove(&nominator); - Self::deposit_event( - RawEvent::NominatorLeft(nominator,validator,nominator_stake,new_total) - ); + >::insert(&candidate, validator); + >::insert(&nominator, nominations); + Self::deposit_event(RawEvent::NominationIncreased(nominator, candidate, before, after)); + Ok(()) + } + #[weight = 0] + fn nominator_bond_less( + origin, + candidate: T::AccountId, + less: BalanceOf + ) -> DispatchResult { + let nominator = ensure_signed(origin)?; + let mut nominations = >::get(&nominator).ok_or(Error::::NominatorDNE)?; + let mut validator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; + let remaining = nominations + .dec_nomination(candidate.clone(), less) + .ok_or(Error::::NominationDNE)? + .ok_or(Error::::Underflow)?; + ensure!(remaining >= T::MinNomination::get(), Error::::NominationBelowMin); + ensure!(nominations.total >= T::MinNominatorStk::get(), Error::::NomBondBelowMin); + T::Currency::unreserve(&nominator, less); + let before = validator.total; + validator.dec_nominator(nominator.clone(), less); + let after = validator.total; + if validator.is_active() { + Self::update_active(candidate.clone(), validator.total); + } + >::insert(&candidate, validator); + >::insert(&nominator, nominations); + Self::deposit_event(RawEvent::NominationDecreased(nominator, candidate, before, after)); Ok(()) } fn on_finalize(n: T::BlockNumber) { @@ -507,49 +867,139 @@ impl Module { >::get().binary_search(acc).is_ok() } // ensure candidate is active before calling - fn update_active_candidate(candidate: T::AccountId, new_total: BalanceOf) { + fn update_active(candidate: T::AccountId, total: BalanceOf) { let mut candidates = >::get(); candidates.remove(&Bond::from_owner(candidate.clone())); candidates.insert(Bond { - owner: candidate.clone(), - amount: new_total, + owner: candidate, + amount: total, }); >::put(candidates); } + fn nominator_joins_validator( + nominator: T::AccountId, + amount: BalanceOf, + validator: T::AccountId, + ) -> DispatchResult { + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + let nomination = Bond { + owner: nominator.clone(), + amount, + }; + ensure!( + state.nominators.insert(nomination), + Error::::NominatorExists + ); + ensure!( + state.nominators.0.len() <= T::MaxNominatorsPerValidator::get(), + Error::::TooManyNominators + ); + T::Currency::reserve(&nominator, amount)?; + let new_total = state.total + amount; + if state.is_active() { + Self::update_active(validator.clone(), new_total); + } + let new_total_locked = >::get() + amount; + >::put(new_total_locked); + state.total = new_total; + >::insert(&validator, state); + Self::deposit_event(RawEvent::ValidatorNominated( + nominator, amount, validator, new_total, + )); + Ok(()) + } + fn nominator_revokes_validator(acc: T::AccountId, validator: T::AccountId) -> DispatchResult { + let mut nominator = >::get(&acc).ok_or(Error::::NominatorDNE)?; + let remaining = nominator + .rm_nomination(validator.clone()) + .ok_or(Error::::NominationDNE)?; + ensure!( + remaining >= T::MinNominatorStk::get(), + Error::::NomBondBelowMin + ); + Self::nominator_leaves_validator(acc.clone(), validator)?; + >::insert(&acc, nominator); + Ok(()) + } + fn nominator_leaves_validator( + nominator: T::AccountId, + validator: T::AccountId, + ) -> DispatchResult { + let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; + let mut exists: Option> = None; + let noms = state + .nominators + .0 + .into_iter() + .filter_map(|nom| { + if nom.owner != nominator { + Some(nom) + } else { + exists = Some(nom.amount); + None + } + }) + .collect(); + let nominators = OrderedSet::from(noms); + let nominator_stake = exists.ok_or(Error::::NominatorDNE)?; + T::Currency::unreserve(&nominator, nominator_stake); + state.nominators = nominators; + state.total -= nominator_stake; + if state.is_active() { + Self::update_active(validator.clone(), state.total); + } + let new_total_locked = >::get() - nominator_stake; + >::put(new_total_locked); + let new_total = state.total; + >::insert(&validator, state); + Self::deposit_event(RawEvent::NominatorLeftValidator( + nominator, + validator, + nominator_stake, + new_total, + )); + Ok(()) + } fn pay_stakers(next: RoundIndex) { + 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(RawEvent::Rewarded(to.clone(), imb.peek())); + } + } + }; let duration = T::BondDuration::get(); if next > duration { let round_to_payout = next - duration; let total = ::get(round_to_payout); - if total == 0u32 { - return; - } let issuance = T::IssuancePerRound::get(); for (val, pts) in >::drain_prefix(round_to_payout) { let pct_due = Perbill::from_rational_approximation(pts, total); let mut amt_due = pct_due * issuance; - if amt_due < T::Currency::minimum_balance() { + if amt_due <= T::Currency::minimum_balance() { continue; } if let Some(state) = >::get(&val) { - if state.nominators.0.len() == 1usize { + if state.nominators.0.is_empty() { // solo validator with no nominators - if let Some(imb) = T::Currency::deposit_into_existing(&val, amt_due).ok() { - Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); - } + mint(amt_due, val.clone()); } else { - let fee = state.fee * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&val, fee).ok() { - Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); - } - amt_due -= fee; + // pay validator first; commission + due_portion + let val_pct = Perbill::from_rational_approximation(state.bond, state.total); + let commission = state.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.0 { let percent = Perbill::from_rational_approximation(amount, state.total); let due = percent * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&owner, due).ok() - { - Self::deposit_event(RawEvent::Rewarded(owner.clone(), imb.peek())); - } + mint(due, owner); } } } @@ -566,14 +1016,26 @@ impl Module { } else { if let Some(state) = >::get(&x.owner) { for bond in state.nominators.0 { - // return funds to nominator + // return stake to nominator T::Currency::unreserve(&bond.owner, bond.amount); + // remove nomination from nominator state + if let Some(mut nominator) = >::get(&bond.owner) { + if let Some(remaining) = nominator.rm_nomination(x.owner.clone()) { + if remaining.is_zero() { + >::remove(&bond.owner); + } else { + >::insert(&bond.owner, nominator); + } + } + } } + // return stake to validator + T::Currency::unreserve(&state.id, state.bond); let new_total = >::get() - state.total; >::put(new_total); >::remove(&x.owner); Self::deposit_event(RawEvent::ValidatorLeft( - x.owner.clone(), + x.owner, state.total, new_total, )); diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 1650549a75..3b3b0ee2f6 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -98,11 +98,13 @@ parameter_types! { pub const BlocksPerRound: u32 = 5; pub const BondDuration: u32 = 2; pub const MaxValidators: u32 = 5; - pub const MaxNominatorsPerValidator: usize = 10; + pub const MaxNominatorsPerValidator: usize = 4; + pub const MaxValidatorsPerNominator: usize = 4; pub const IssuancePerRound: u128 = 10; pub const MaxFee: Perbill = Perbill::from_percent(50); pub const MinValidatorStk: u128 = 10; pub const MinNominatorStk: u128 = 5; + pub const MinNomination: u128 = 3; } impl Config for Test { type Event = MetaEvent; @@ -111,21 +113,37 @@ impl Config for Test { type BondDuration = BondDuration; type MaxValidators = MaxValidators; type MaxNominatorsPerValidator = MaxNominatorsPerValidator; + type MaxValidatorsPerNominator = MaxValidatorsPerNominator; type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; type MinNominatorStk = MinNominatorStk; + type MinNomination = MinNomination; } pub type Balances = pallet_balances::Module; pub type Stake = Module; pub type Sys = frame_system::Module; -pub fn genesis() -> sp_io::TestExternalities { +fn genesis( + balances: Vec<(AccountId, Balance)>, + stakers: Vec<(AccountId, Option, Balance)>, +) -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); - let genesis = pallet_balances::GenesisConfig:: { - balances: vec![ + let genesis = pallet_balances::GenesisConfig:: { balances }; + genesis.assimilate_storage(&mut storage).unwrap(); + GenesisConfig:: { stakers } + .assimilate_storage(&mut storage) + .unwrap(); + let mut ext = sp_io::TestExternalities::from(storage); + ext.execute_with(|| Sys::set_block_number(1)); + ext +} + +pub(crate) fn two_validators_four_nominators() -> sp_io::TestExternalities { + genesis( + vec![ (1, 1000), (2, 300), (3, 100), @@ -136,10 +154,7 @@ pub fn genesis() -> sp_io::TestExternalities { (8, 9), (9, 4), ], - }; - genesis.assimilate_storage(&mut storage).unwrap(); - GenesisConfig:: { - stakers: vec![ + vec![ // validators (1, None, 500), (2, None, 200), @@ -149,20 +164,12 @@ pub fn genesis() -> sp_io::TestExternalities { (5, Some(2), 100), (6, Some(2), 100), ], - } - .assimilate_storage(&mut storage) - .unwrap(); - let mut ext = sp_io::TestExternalities::from(storage); - ext.execute_with(|| Sys::set_block_number(1)); - ext + ) } -pub fn genesis2() -> sp_io::TestExternalities { - let mut storage = frame_system::GenesisConfig::default() - .build_storage::() - .unwrap(); - let genesis = pallet_balances::GenesisConfig:: { - balances: vec![ +pub(crate) fn five_validators_no_nominators() -> sp_io::TestExternalities { + genesis( + vec![ (1, 1000), (2, 1000), (3, 1000), @@ -173,10 +180,7 @@ pub fn genesis2() -> sp_io::TestExternalities { (8, 33), (9, 33), ], - }; - genesis.assimilate_storage(&mut storage).unwrap(); - GenesisConfig:: { - stakers: vec![ + vec![ // validators (1, None, 100), (2, None, 90), @@ -185,15 +189,54 @@ pub fn genesis2() -> sp_io::TestExternalities { (5, None, 60), (6, None, 50), ], - } - .assimilate_storage(&mut storage) - .unwrap(); - let mut ext = sp_io::TestExternalities::from(storage); - ext.execute_with(|| Sys::set_block_number(1)); - ext + ) } -pub fn roll_to(n: u64) { +pub(crate) fn five_validators_five_nominators() -> sp_io::TestExternalities { + genesis( + vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + ], + vec![ + // validators + (1, None, 20), + (2, None, 20), + (3, None, 20), + (4, None, 20), + (5, None, 10), + // nominators + (6, Some(1), 10), + (7, Some(1), 10), + (8, Some(2), 10), + (9, Some(2), 10), + (10, Some(1), 10), + ], + ) +} + +pub(crate) fn one_validator_two_nominators() -> sp_io::TestExternalities { + genesis( + vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)], + vec![ + // validators + (1, None, 20), + // nominators + (2, Some(1), 10), + (3, Some(1), 10), + ], + ) +} + +pub(crate) fn roll_to(n: u64) { while Sys::block_number() < n { Stake::on_finalize(Sys::block_number()); Balances::on_finalize(Sys::block_number()); @@ -205,6 +248,26 @@ pub fn roll_to(n: u64) { } } -pub fn last_event() -> MetaEvent { +pub(crate) fn last_event() -> MetaEvent { Sys::events().pop().expect("Event expected").event } + +pub(crate) fn events() -> Vec> { + Sys::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let MetaEvent::stake(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>() +} + +// Same storage changes as EventHandler::note_author impl +pub(crate) fn set_author(round: u32, acc: u64, pts: u32) { + ::Points::mutate(round, |p| *p += pts); + ::AwardedPts::mutate(round, acc, |p| *p += pts); +} diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index 931e351809..5249b05cc9 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -21,8 +21,8 @@ use mock::*; use sp_runtime::DispatchError; #[test] -fn genesis_config_works() { - genesis().execute_with(|| { +fn genesis_works() { + two_validators_four_nominators().execute_with(|| { assert!(Sys::events().is_empty()); // validators assert_eq!(Balances::reserved_balance(&1), 500); @@ -48,11 +48,29 @@ fn genesis_config_works() { assert_eq!(Balances::free_balance(&9), 4); assert_eq!(Balances::reserved_balance(&9), 0); }); + five_validators_five_nominators().execute_with(|| { + assert!(Sys::events().is_empty()); + // validators + 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); + } + }); } #[test] fn online_offline_behaves() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { roll_to(4); assert_noop!( Stake::go_offline(Origin::signed(3)), @@ -73,17 +91,6 @@ fn online_offline_behaves() { MetaEvent::stake(RawEvent::ValidatorWentOffline(3, 2)) ); roll_to(21); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); let mut expected = vec![ RawEvent::ValidatorChosen(2, 1, 700), RawEvent::ValidatorChosen(2, 2, 400), @@ -97,7 +104,7 @@ fn online_offline_behaves() { RawEvent::ValidatorChosen(5, 1, 700), RawEvent::NewRound(20, 5, 1, 700), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); assert_noop!( Stake::go_offline(Origin::signed(2)), Error::::AlreadyOffline @@ -112,24 +119,13 @@ fn online_offline_behaves() { expected.push(RawEvent::ValidatorChosen(6, 1, 700)); expected.push(RawEvent::ValidatorChosen(6, 2, 400)); expected.push(RawEvent::NewRound(25, 6, 2, 1100)); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } #[test] fn join_validator_candidates_works() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { assert_noop!( Stake::join_candidates(Origin::signed(1), Perbill::from_percent(2), 11u128,), Error::::CandidateExists @@ -169,7 +165,7 @@ fn join_validator_candidates_works() { #[test] fn validator_exit_executes_after_delay() { - genesis().execute_with(|| { + two_validators_four_nominators().execute_with(|| { roll_to(4); assert_noop!( Stake::leave_candidates(Origin::signed(3)), @@ -184,17 +180,6 @@ fn validator_exit_executes_after_delay() { let info = ::Candidates::get(&2).unwrap(); assert_eq!(info.state, ValidatorStatus::Leaving(5)); roll_to(21); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // we must exclude leaving validators from rewards while // holding them retroactively accountable for previous faults // (within the last T::SlashingWindow blocks) @@ -212,26 +197,15 @@ fn validator_exit_executes_after_delay() { RawEvent::ValidatorChosen(5, 1, 700), RawEvent::NewRound(20, 5, 1, 700), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } #[test] fn validator_selection_chooses_top_candidates() { - genesis2().execute_with(|| { + five_validators_no_nominators().execute_with(|| { roll_to(4); roll_to(8); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -241,7 +215,7 @@ fn validator_selection_chooses_top_candidates() { RawEvent::ValidatorChosen(2, 5, 60), RawEvent::NewRound(5, 2, 5, 400), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); assert_ok!(Stake::leave_candidates(Origin::signed(6))); assert_eq!( last_event(), @@ -258,17 +232,6 @@ fn validator_selection_chooses_top_candidates() { MetaEvent::stake(RawEvent::JoinedValidatorCandidates(6, 69u128, 469u128)) ); roll_to(27); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -305,26 +268,15 @@ fn validator_selection_chooses_top_candidates() { RawEvent::ValidatorChosen(6, 6, 69), RawEvent::NewRound(25, 6, 5, 409), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } #[test] fn exit_queue_works() { - genesis2().execute_with(|| { + five_validators_no_nominators().execute_with(|| { roll_to(4); roll_to(8); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let mut expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -334,7 +286,7 @@ fn exit_queue_works() { RawEvent::ValidatorChosen(2, 5, 60), RawEvent::NewRound(5, 2, 5, 400), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); assert_ok!(Stake::leave_candidates(Origin::signed(6))); assert_eq!( last_event(), @@ -352,18 +304,11 @@ fn exit_queue_works() { last_event(), MetaEvent::stake(RawEvent::ValidatorScheduledExit(4, 4, 6)) ); + assert_noop!( + Stake::leave_candidates(Origin::signed(4)), + Error::::AlreadyLeaving + ); roll_to(21); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); let mut new_events = vec![ RawEvent::ValidatorScheduledExit(2, 6, 4), RawEvent::ValidatorChosen(3, 1, 100), @@ -387,31 +332,15 @@ fn exit_queue_works() { RawEvent::NewRound(20, 5, 3, 270), ]; expected.append(&mut new_events); - assert_eq!(events, expected); + assert_eq!(events(), expected); }); } #[test] -fn payout_distribution_works() { - genesis2().execute_with(|| { - // same storage changes as EventHandler::note_author impl - fn set_pts(round: u32, acc: u64, pts: u32) { - ::Points::mutate(round, |p| *p += pts); - ::AwardedPts::insert(round, acc, pts); - } +fn payout_distribution_to_solo_validators() { + five_validators_no_nominators().execute_with(|| { roll_to(4); roll_to(8); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // should choose top MaxValidators (5), in order let mut expected = vec![ RawEvent::ValidatorChosen(2, 1, 100), @@ -421,21 +350,10 @@ fn payout_distribution_works() { RawEvent::ValidatorChosen(2, 5, 60), RawEvent::NewRound(5, 2, 5, 400), ]; - assert_eq!(events, expected); + assert_eq!(events(), expected); // ~ set block author as 1 for all blocks this round - set_pts(2, 1, 100); + set_author(2, 1, 100); roll_to(16); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // pay total issuance (=10) to 1 let mut new = vec![ RawEvent::ValidatorChosen(3, 1, 100), @@ -453,23 +371,12 @@ fn payout_distribution_works() { RawEvent::NewRound(15, 4, 5, 400), ]; expected.append(&mut new); - assert_eq!(events, expected); + assert_eq!(events(), expected); // ~ set block author as 1 for 3 blocks this round - set_pts(4, 1, 60); + set_author(4, 1, 60); // ~ set block author as 2 for 2 blocks this round - set_pts(4, 2, 40); + set_author(4, 2, 40); roll_to(26); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // pay 60% total issuance to 1 and 40% total issuance to 2 let mut new1 = vec![ RawEvent::ValidatorChosen(5, 1, 100), @@ -488,25 +395,14 @@ fn payout_distribution_works() { RawEvent::NewRound(25, 6, 5, 400), ]; expected.append(&mut new1); - assert_eq!(events, expected); + assert_eq!(events(), expected); // ~ each validator produces 1 block this round - set_pts(6, 1, 20); - set_pts(6, 2, 20); - set_pts(6, 3, 20); - set_pts(6, 4, 20); - set_pts(6, 5, 20); + set_author(6, 1, 20); + set_author(6, 2, 20); + set_author(6, 3, 20); + set_author(6, 4, 20); + set_author(6, 5, 20); roll_to(36); - let events = Sys::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| { - if let MetaEvent::stake(inner) = e { - Some(inner) - } else { - None - } - }) - .collect::>(); // pay 20% issuance for all validators let mut new2 = vec![ RawEvent::ValidatorChosen(7, 1, 100), @@ -528,7 +424,7 @@ fn payout_distribution_works() { RawEvent::NewRound(35, 8, 5, 400), ]; expected.append(&mut new2); - assert_eq!(events, expected); + assert_eq!(events(), expected); // check that distributing rewards clears awarded pts assert!(::AwardedPts::get(1, 1).is_zero()); assert!(::AwardedPts::get(4, 1).is_zero()); @@ -540,3 +436,447 @@ fn payout_distribution_works() { assert!(::AwardedPts::get(6, 5).is_zero()); }); } + +#[test] +fn payout_distribution_to_nominators() { + five_validators_five_nominators().execute_with(|| { + roll_to(4); + roll_to(8); + // chooses top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 50), + RawEvent::ValidatorChosen(2, 2, 40), + RawEvent::ValidatorChosen(2, 4, 20), + RawEvent::ValidatorChosen(2, 3, 20), + RawEvent::ValidatorChosen(2, 5, 10), + RawEvent::NewRound(5, 2, 5, 140), + ]; + assert_eq!(events(), expected); + // ~ set block author as 1 for all blocks this round + set_author(2, 1, 100); + roll_to(16); + // distribute total issuance (=10) to validator 1 and its nominators 6, 7, 19 + // -> NOTE that no fee is taken because validators at genesis set default 2% fee + // and 2% of 10 is ~0 by the Perbill arithmetic + let mut new = vec![ + RawEvent::ValidatorChosen(3, 1, 50), + RawEvent::ValidatorChosen(3, 2, 40), + RawEvent::ValidatorChosen(3, 4, 20), + RawEvent::ValidatorChosen(3, 3, 20), + RawEvent::ValidatorChosen(3, 5, 10), + RawEvent::NewRound(10, 3, 5, 140), + RawEvent::Rewarded(1, 4), + RawEvent::Rewarded(6, 2), + RawEvent::Rewarded(7, 2), + RawEvent::Rewarded(10, 2), + RawEvent::ValidatorChosen(4, 1, 50), + RawEvent::ValidatorChosen(4, 2, 40), + RawEvent::ValidatorChosen(4, 4, 20), + RawEvent::ValidatorChosen(4, 3, 20), + RawEvent::ValidatorChosen(4, 5, 10), + RawEvent::NewRound(15, 4, 5, 140), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + }); +} + +#[test] +fn pays_validator_commission() { + one_validator_two_nominators().execute_with(|| { + roll_to(4); + roll_to(8); + // chooses top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 40), + RawEvent::NewRound(5, 2, 1, 40), + ]; + assert_eq!(events(), expected); + assert_ok!(Stake::join_candidates( + Origin::signed(4), + Perbill::from_percent(20), + 20u128 + )); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::JoinedValidatorCandidates(4, 20u128, 60u128)) + ); + roll_to(9); + assert_ok!(Stake::join_nominators(Origin::signed(5), 4, 10)); + assert_ok!(Stake::join_nominators(Origin::signed(6), 4, 10)); + roll_to(11); + let mut new = vec![ + RawEvent::JoinedValidatorCandidates(4, 20, 60), + RawEvent::ValidatorNominated(5, 10, 4, 30), + RawEvent::NominatorJoined(5, 10), + RawEvent::ValidatorNominated(6, 10, 4, 40), + RawEvent::NominatorJoined(6, 10), + RawEvent::ValidatorChosen(3, 4, 40), + RawEvent::ValidatorChosen(3, 1, 40), + RawEvent::NewRound(10, 3, 2, 80), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + // only reward author with id 4 + set_author(3, 4, 100); + roll_to(21); + // 20% of 10 is commission + due_portion (4) = 2 + 4 = 6 + // all nominator payouts are 10-2 = 8 * stake_pct + let mut new2 = vec![ + RawEvent::ValidatorChosen(4, 4, 40), + RawEvent::ValidatorChosen(4, 1, 40), + RawEvent::NewRound(15, 4, 2, 80), + RawEvent::Rewarded(4, 6), + RawEvent::Rewarded(5, 2), + RawEvent::Rewarded(6, 2), + RawEvent::ValidatorChosen(5, 4, 40), + RawEvent::ValidatorChosen(5, 1, 40), + RawEvent::NewRound(20, 5, 2, 80), + ]; + expected.append(&mut new2); + assert_eq!(events(), expected); + }); +} + +#[test] +fn multiple_nominations() { + five_validators_five_nominators().execute_with(|| { + roll_to(4); + roll_to(8); + // chooses top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 50), + RawEvent::ValidatorChosen(2, 2, 40), + RawEvent::ValidatorChosen(2, 4, 20), + RawEvent::ValidatorChosen(2, 3, 20), + RawEvent::ValidatorChosen(2, 5, 10), + RawEvent::NewRound(5, 2, 5, 140), + ]; + assert_eq!(events(), expected); + assert_noop!( + Stake::nominate_new(Origin::signed(5), 2, 10), + Error::::NominatorDNE, + ); + assert_noop!( + Stake::nominate_new(Origin::signed(11), 1, 10), + Error::::NominatorDNE, + ); + assert_noop!( + Stake::nominate_new(Origin::signed(6), 1, 10), + Error::::AlreadyNominatedValidator, + ); + assert_noop!( + Stake::nominate_new(Origin::signed(6), 2, 2), + Error::::NominationBelowMin, + ); + assert_ok!(Stake::nominate_new(Origin::signed(6), 2, 10)); + assert_ok!(Stake::nominate_new(Origin::signed(6), 3, 10)); + assert_ok!(Stake::nominate_new(Origin::signed(6), 4, 10)); + assert_noop!( + Stake::nominate_new(Origin::signed(6), 5, 10), + Error::::ExceedMaxValidatorsPerNom, + ); + roll_to(16); + let mut new = vec![ + RawEvent::ValidatorNominated(6, 10, 2, 50), + RawEvent::ValidatorNominated(6, 10, 3, 30), + RawEvent::ValidatorNominated(6, 10, 4, 30), + RawEvent::ValidatorChosen(3, 2, 50), + RawEvent::ValidatorChosen(3, 1, 50), + RawEvent::ValidatorChosen(3, 4, 30), + RawEvent::ValidatorChosen(3, 3, 30), + RawEvent::ValidatorChosen(3, 5, 10), + RawEvent::NewRound(10, 3, 5, 170), + RawEvent::ValidatorChosen(4, 2, 50), + RawEvent::ValidatorChosen(4, 1, 50), + RawEvent::ValidatorChosen(4, 4, 30), + RawEvent::ValidatorChosen(4, 3, 30), + RawEvent::ValidatorChosen(4, 5, 10), + RawEvent::NewRound(15, 4, 5, 170), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + roll_to(21); + assert_ok!(Stake::nominate_new(Origin::signed(7), 2, 80)); + assert_noop!( + Stake::nominate_new(Origin::signed(7), 3, 11), + DispatchError::Module { + index: 0, + error: 3, + message: Some("InsufficientBalance") + }, + ); + assert_noop!( + Stake::nominate_new(Origin::signed(10), 2, 10), + Error::::TooManyNominators + ); + roll_to(26); + let mut new2 = vec![ + RawEvent::ValidatorChosen(5, 2, 50), + RawEvent::ValidatorChosen(5, 1, 50), + RawEvent::ValidatorChosen(5, 4, 30), + RawEvent::ValidatorChosen(5, 3, 30), + RawEvent::ValidatorChosen(5, 5, 10), + RawEvent::NewRound(20, 5, 5, 170), + RawEvent::ValidatorNominated(7, 80, 2, 130), + RawEvent::ValidatorChosen(6, 2, 130), + RawEvent::ValidatorChosen(6, 1, 50), + RawEvent::ValidatorChosen(6, 4, 30), + RawEvent::ValidatorChosen(6, 3, 30), + RawEvent::ValidatorChosen(6, 5, 10), + RawEvent::NewRound(25, 6, 5, 250), + ]; + expected.append(&mut new2); + assert_eq!(events(), expected); + assert_ok!(Stake::leave_candidates(Origin::signed(2))); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::ValidatorScheduledExit(6, 2, 8)) + ); + roll_to(31); + let mut new3 = vec![ + RawEvent::ValidatorScheduledExit(6, 2, 8), + RawEvent::ValidatorChosen(7, 1, 50), + RawEvent::ValidatorChosen(7, 4, 30), + RawEvent::ValidatorChosen(7, 3, 30), + RawEvent::ValidatorChosen(7, 5, 10), + RawEvent::NewRound(30, 7, 4, 120), + ]; + expected.append(&mut new3); + assert_eq!(events(), expected); + // verify that nominations are removed after validator leaves, not before + assert_eq!(::Nominators::get(7).unwrap().total, 90); + assert_eq!( + ::Nominators::get(7) + .unwrap() + .nominations + .0 + .len(), + 2usize + ); + assert_eq!(::Nominators::get(6).unwrap().total, 40); + assert_eq!( + ::Nominators::get(6) + .unwrap() + .nominations + .0 + .len(), + 4usize + ); + assert_eq!(Balances::reserved_balance(&6), 40); + assert_eq!(Balances::reserved_balance(&7), 90); + assert_eq!(Balances::free_balance(&6), 60); + assert_eq!(Balances::free_balance(&7), 10); + roll_to(40); + assert_eq!(::Nominators::get(7).unwrap().total, 10); + assert_eq!(::Nominators::get(6).unwrap().total, 30); + assert_eq!( + ::Nominators::get(7) + .unwrap() + .nominations + .0 + .len(), + 1usize + ); + assert_eq!( + ::Nominators::get(6) + .unwrap() + .nominations + .0 + .len(), + 3usize + ); + assert_eq!(Balances::reserved_balance(&6), 30); + assert_eq!(Balances::reserved_balance(&7), 10); + assert_eq!(Balances::free_balance(&6), 70); + assert_eq!(Balances::free_balance(&7), 90); + }); +} + +#[test] +fn validators_bond_more_less() { + five_validators_five_nominators().execute_with(|| { + roll_to(4); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(6), 50), + Error::::CandidateDNE + ); + assert_ok!(Stake::candidate_bond_more(Origin::signed(1), 50)); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(1), 40), + DispatchError::Module { + index: 0, + error: 3, + message: Some("InsufficientBalance") + } + ); + assert_ok!(Stake::leave_candidates(Origin::signed(1))); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(1), 30), + Error::::CannotActivateIfLeaving + ); + roll_to(30); + assert_noop!( + Stake::candidate_bond_more(Origin::signed(1), 40), + Error::::CandidateDNE + ); + assert_ok!(Stake::candidate_bond_more(Origin::signed(2), 80)); + assert_ok!(Stake::candidate_bond_less(Origin::signed(2), 90)); + assert_ok!(Stake::candidate_bond_less(Origin::signed(3), 10)); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(2), 11), + Error::::Underflow + ); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(2), 1), + Error::::ValBondBelowMin + ); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(3), 1), + Error::::ValBondBelowMin + ); + assert_noop!( + Stake::candidate_bond_less(Origin::signed(4), 11), + Error::::ValBondBelowMin + ); + assert_ok!(Stake::candidate_bond_less(Origin::signed(4), 10)); + }); +} + +#[test] +fn nominators_bond_more_less() { + five_validators_five_nominators().execute_with(|| { + roll_to(4); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(1), 2, 50), + Error::::NominatorDNE + ); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(6), 2, 50), + Error::::NominationDNE + ); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(7), 6, 50), + Error::::CandidateDNE + ); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 1, 11), + Error::::Underflow + ); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 1, 8), + Error::::NominationBelowMin + ); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 1, 6), + Error::::NomBondBelowMin + ); + assert_ok!(Stake::nominator_bond_more(Origin::signed(6), 1, 10)); + assert_noop!( + Stake::nominator_bond_less(Origin::signed(6), 2, 5), + Error::::NominationDNE + ); + assert_noop!( + Stake::nominator_bond_more(Origin::signed(6), 1, 81), + DispatchError::Module { + index: 0, + error: 3, + message: Some("InsufficientBalance") + } + ); + roll_to(9); + assert_eq!(Balances::reserved_balance(&6), 20); + assert_ok!(Stake::leave_candidates(Origin::signed(1))); + roll_to(31); + assert!(!Stake::is_nominator(&6)); + assert_eq!(Balances::reserved_balance(&6), 0); + assert_eq!(Balances::free_balance(&6), 100); + }); +} + +#[test] +fn switch_nomination_works() { + five_validators_five_nominators().execute_with(|| { + roll_to(4); + roll_to(8); + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 50), + RawEvent::ValidatorChosen(2, 2, 40), + RawEvent::ValidatorChosen(2, 4, 20), + RawEvent::ValidatorChosen(2, 3, 20), + RawEvent::ValidatorChosen(2, 5, 10), + RawEvent::NewRound(5, 2, 5, 140), + ]; + assert_eq!(events(), expected); + assert_noop!( + Stake::switch_nomination(Origin::signed(1), 1, 2), + Error::::NominatorDNE + ); + assert_noop!( + Stake::switch_nomination(Origin::signed(6), 1, 7), + Error::::CandidateDNE + ); + assert_noop!( + Stake::switch_nomination(Origin::signed(6), 2, 1), + Error::::NominationDNE + ); + assert_noop!( + Stake::switch_nomination(Origin::signed(6), 1, 1), + Error::::CannotSwitchToSameNomination + ); + assert_ok!(Stake::switch_nomination(Origin::signed(6), 1, 2)); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::NominationSwapped(6, 10, 1, 2)) + ); + assert_ok!(Stake::switch_nomination(Origin::signed(7), 1, 2)); + assert_ok!(Stake::switch_nomination(Origin::signed(8), 2, 1)); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::NominationSwapped(8, 10, 2, 1)) + ); + assert_ok!(Stake::switch_nomination(Origin::signed(9), 2, 1)); + assert_ok!(Stake::switch_nomination(Origin::signed(10), 1, 2)); + assert_eq!( + last_event(), + MetaEvent::stake(RawEvent::NominationSwapped(10, 10, 1, 2)) + ); + // verify nothing changed with roles or balances since genesis + 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); + for x in 6..11 { + assert!(Stake::is_nominator(&x)); + assert_eq!(Balances::free_balance(&x), 90); + assert_eq!(Balances::reserved_balance(&x), 10); + } + roll_to(10); + roll_to(16); + let mut new = vec![ + RawEvent::NominationSwapped(6, 10, 1, 2), + RawEvent::NominationSwapped(7, 10, 1, 2), + RawEvent::NominationSwapped(8, 10, 2, 1), + RawEvent::NominationSwapped(9, 10, 2, 1), + RawEvent::NominationSwapped(10, 10, 1, 2), + RawEvent::ValidatorChosen(3, 2, 50), + RawEvent::ValidatorChosen(3, 1, 40), + RawEvent::ValidatorChosen(3, 4, 20), + RawEvent::ValidatorChosen(3, 3, 20), + RawEvent::ValidatorChosen(3, 5, 10), + RawEvent::NewRound(10, 3, 5, 140), + RawEvent::ValidatorChosen(4, 2, 50), + RawEvent::ValidatorChosen(4, 1, 40), + RawEvent::ValidatorChosen(4, 4, 20), + RawEvent::ValidatorChosen(4, 3, 20), + RawEvent::ValidatorChosen(4, 5, 10), + RawEvent::NewRound(15, 4, 5, 140), + ]; + expected.append(&mut new); + assert_eq!(events(), expected); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e83eb8b641..63241138cc 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -310,6 +310,8 @@ parameter_types! { pub const MaxValidators: u32 = 8; /// Maximum 10 nominators per validator pub const MaxNominatorsPerValidator: usize = 10; + /// Maximum 8 validators per nominator (same as MaxValidators) + pub const MaxValidatorsPerNominator: usize = 8; /// Issue 49 new tokens as rewards to validators every 2 minutes (round) pub const IssuancePerRound: u128 = 49 * GLMR; /// The maximum percent a validator can take off the top of its rewards is 50% @@ -326,9 +328,11 @@ impl stake::Config for Runtime { type BondDuration = BondDuration; type MaxValidators = MaxValidators; type MaxNominatorsPerValidator = MaxNominatorsPerValidator; + type MaxValidatorsPerNominator = MaxValidatorsPerNominator; type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; + type MinNomination = MinNominatorStk; type MinNominatorStk = MinNominatorStk; } impl author_inherent::Config for Runtime { diff --git a/tests/tests/test-stake.ts b/tests/tests/test-stake.ts index 54ce683586..88442c122d 100644 --- a/tests/tests/test-stake.ts +++ b/tests/tests/test-stake.ts @@ -18,25 +18,29 @@ describeWithMoonbeam("Moonbeam RPC (Stake)", `simple-specs.json`, (context) => { }); step("issuance minted to the sole validator for authoring blocks", async function () { - const expectedBalance = BigInt(GENESIS_ACCOUNT_BALANCE) + 49n * GLMR; - const expectedBalance2 = expectedBalance + 49n * GLMR; + const issuanceEveryRound = 49n * GLMR; + // payment transfer is delayed by two rounds + const balanceAfterBlock40 = BigInt(GENESIS_ACCOUNT_BALANCE) + issuanceEveryRound; + const balanceAfterBlock60 = balanceAfterBlock40 + issuanceEveryRound; var block = await context.web3.eth.getBlockNumber(); while (block < 40) { await createAndFinalizeBlock(context.polkadotApi); block = await context.web3.eth.getBlockNumber(); } - expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal(expectedBalance.toString()); + expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal( + balanceAfterBlock40.toString() + ); while (block < 60) { await createAndFinalizeBlock(context.polkadotApi); block = await context.web3.eth.getBlockNumber(); } expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal( - expectedBalance2.toString() + balanceAfterBlock60.toString() ); }); it("candidates set in genesis", async function () { const candidates = await context.polkadotApi.query.stake.candidates(GENESIS_ACCOUNT); - expect((candidates.toHuman() as any).validator.toLowerCase()).equal(GENESIS_ACCOUNT); + expect((candidates.toHuman() as any).id.toLowerCase()).equal(GENESIS_ACCOUNT); }); });