#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode};
use fabric_support::{
decl_error, decl_event, decl_module, decl_storage,
dispatch::{DispatchResultWithPostInfo, WithPostDispatchInfo},
ensure,
storage::{IterableStorageMap, StorageMap},
traits::{
ChangeMembers, Contains, ContainsLengthBound, Currency, CurrencyToVote, Get,
InitializeMembers, LockIdentifier, LockableCurrency, OnUnbalanced, ReservableCurrency,
WithdrawReasons,
},
weights::Weight,
};
use fabric_system::{ensure_root, ensure_signed};
use tp_npos_elections::{ElectionResult, ExtendedBalance};
use tp_runtime::{
traits::{Saturating, StaticLookup, Zero},
DispatchError, Perbill, RuntimeDebug,
};
use tetcore_std::{prelude::*, cmp::Ordering};
mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;
pub mod migrations_3_0_0;
pub const MAXIMUM_VOTE: usize = 16;
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as fabric_system::Config>::AccountId>>::Balance;
type NegativeImbalanceOf<T> =
<<T as Config>::Currency as Currency<<T as fabric_system::Config>::AccountId>>::NegativeImbalance;
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)]
pub enum Renouncing {
Member,
RunnerUp,
Candidate(#[codec(compact)] u32),
}
#[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq)]
pub struct Voter<AccountId, Balance> {
pub votes: Vec<AccountId>,
pub stake: Balance,
pub deposit: Balance,
}
#[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq)]
pub struct SeatHolder<AccountId, Balance> {
pub who: AccountId,
pub stake: Balance,
pub deposit: Balance,
}
pub trait Config: fabric_system::Config {
type Event: From<Event<Self>> + Into<<Self as fabric_system::Config>::Event>;
type ModuleId: Get<LockIdentifier>;
type Currency:
LockableCurrency<Self::AccountId, Moment=Self::BlockNumber> +
ReservableCurrency<Self::AccountId>;
type ChangeMembers: ChangeMembers<Self::AccountId>;
type InitializeMembers: InitializeMembers<Self::AccountId>;
type CurrencyToVote: CurrencyToVote<BalanceOf<Self>>;
type CandidacyBond: Get<BalanceOf<Self>>;
type VotingBondBase: Get<BalanceOf<Self>>;
type VotingBondFactor: Get<BalanceOf<Self>>;
type LoserCandidate: OnUnbalanced<NegativeImbalanceOf<Self>>;
type KickedMember: OnUnbalanced<NegativeImbalanceOf<Self>>;
type DesiredMembers: Get<u32>;
type DesiredRunnersUp: Get<u32>;
type TermDuration: Get<Self::BlockNumber>;
type WeightInfo: WeightInfo;
}
decl_storage! {
trait Store for Module<T: Config> as PhragmenElection {
pub Members get(fn members): Vec<SeatHolder<T::AccountId, BalanceOf<T>>>;
pub RunnersUp get(fn runners_up): Vec<SeatHolder<T::AccountId, BalanceOf<T>>>;
pub Candidates get(fn candidates): Vec<(T::AccountId, BalanceOf<T>)>;
pub ElectionRounds get(fn election_rounds): u32 = Zero::zero();
pub Voting get(fn voting): map hasher(twox_64_concat) T::AccountId => Voter<T::AccountId, BalanceOf<T>>;
} add_extra_genesis {
config(members): Vec<(T::AccountId, BalanceOf<T>)>;
build(|config: &GenesisConfig<T>| {
assert!(
config.members.len() as u32 <= T::DesiredMembers::get(),
"Cannot accept more than DesiredMembers genesis member",
);
let members = config.members.iter().map(|(ref member, ref stake)| {
assert!(
T::Currency::free_balance(member) >= *stake,
"Genesis member does not have enough stake.",
);
Members::<T>::mutate(|members| {
match members.binary_search_by(|m| m.who.cmp(member)) {
Ok(_) => panic!("Duplicate member in elections-phragmen genesis: {}", member),
Err(pos) => members.insert(
pos,
SeatHolder { who: member.clone(), stake: *stake, deposit: Zero::zero() },
),
}
});
<Voting<T>>::insert(
&member,
Voter { votes: vec![member.clone()], stake: *stake, deposit: Zero::zero() },
);
member.clone()
}).collect::<Vec<T::AccountId>>();
T::InitializeMembers::initialize_members(&members);
})
}
}
decl_error! {
pub enum Error for Module<T: Config> {
UnableToVote,
NoVotes,
TooManyVotes,
MaximumVotesExceeded,
LowBalance,
UnableToPayBond,
MustBeVoter,
ReportSelf,
DuplicatedCandidate,
MemberSubmit,
RunnerUpSubmit,
InsufficientCandidateFunds,
NotMember,
InvalidWitnessData,
InvalidVoteCount,
InvalidRenouncing,
InvalidReplacement,
}
}
decl_event!(
pub enum Event<T> where Balance = BalanceOf<T>, <T as fabric_system::Config>::AccountId {
NewTerm(Vec<(AccountId, Balance)>),
EmptyTerm,
ElectionError,
MemberKicked(AccountId),
Renounced(AccountId),
CandidateSlashed(AccountId, Balance),
SeatHolderSlashed(AccountId, Balance),
}
);
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
const CandidacyBond: BalanceOf<T> = T::CandidacyBond::get();
const VotingBondBase: BalanceOf<T> = T::VotingBondBase::get();
const VotingBondFactor: BalanceOf<T> = T::VotingBondFactor::get();
const DesiredMembers: u32 = T::DesiredMembers::get();
const DesiredRunnersUp: u32 = T::DesiredRunnersUp::get();
const TermDuration: T::BlockNumber = T::TermDuration::get();
const ModuleId: LockIdentifier = T::ModuleId::get();
#[weight =
T::WeightInfo::vote_more(votes.len() as u32)
.max(T::WeightInfo::vote_less(votes.len() as u32))
.max(T::WeightInfo::vote_equal(votes.len() as u32))
]
fn vote(
origin,
votes: Vec<T::AccountId>,
#[compact] value: BalanceOf<T>,
) {
let who = ensure_signed(origin)?;
ensure!(votes.len() <= MAXIMUM_VOTE, Error::<T>::MaximumVotesExceeded);
ensure!(!votes.is_empty(), Error::<T>::NoVotes);
let candidates_count = <Candidates<T>>::decode_len().unwrap_or(0);
let members_count = <Members<T>>::decode_len().unwrap_or(0);
let runners_up_count = <RunnersUp<T>>::decode_len().unwrap_or(0);
let allowed_votes = candidates_count
.saturating_add(members_count)
.saturating_add(runners_up_count);
ensure!(!allowed_votes.is_zero(), Error::<T>::UnableToVote);
ensure!(votes.len() <= allowed_votes, Error::<T>::TooManyVotes);
ensure!(value > T::Currency::minimum_balance(), Error::<T>::LowBalance);
let new_deposit = Self::deposit_of(votes.len());
let Voter { deposit: old_deposit, .. } = <Voting<T>>::get(&who);
match new_deposit.cmp(&old_deposit) {
Ordering::Greater => {
let to_reserve = new_deposit - old_deposit;
T::Currency::reserve(&who, to_reserve).map_err(|_| Error::<T>::UnableToPayBond)?;
},
Ordering::Equal => {},
Ordering::Less => {
let to_unreserve = old_deposit - new_deposit;
let _remainder = T::Currency::unreserve(&who, to_unreserve);
debug_assert!(_remainder.is_zero());
},
};
let locked_stake = value.min(T::Currency::total_balance(&who));
T::Currency::set_lock(
T::ModuleId::get(),
&who,
locked_stake,
WithdrawReasons::all(),
);
Voting::<T>::insert(&who, Voter { votes, deposit: new_deposit, stake: locked_stake });
}
#[weight = T::WeightInfo::remove_voter()]
fn remove_voter(origin) {
let who = ensure_signed(origin)?;
ensure!(Self::is_voter(&who), Error::<T>::MustBeVoter);
Self::do_remove_voter(&who);
}
#[weight = T::WeightInfo::submit_candidacy(*candidate_count)]
fn submit_candidacy(origin, #[compact] candidate_count: u32) {
let who = ensure_signed(origin)?;
let actual_count = <Candidates<T>>::decode_len().unwrap_or(0);
ensure!(
actual_count as u32 <= candidate_count,
Error::<T>::InvalidWitnessData,
);
let index = Self::is_candidate(&who).err().ok_or(Error::<T>::DuplicatedCandidate)?;
ensure!(!Self::is_member(&who), Error::<T>::MemberSubmit);
ensure!(!Self::is_runner_up(&who), Error::<T>::RunnerUpSubmit);
T::Currency::reserve(&who, T::CandidacyBond::get())
.map_err(|_| Error::<T>::InsufficientCandidateFunds)?;
<Candidates<T>>::mutate(|c| c.insert(index, (who, T::CandidacyBond::get())));
}
#[weight = match *renouncing {
Renouncing::Candidate(count) => T::WeightInfo::renounce_candidacy_candidate(count),
Renouncing::Member => T::WeightInfo::renounce_candidacy_members(),
Renouncing::RunnerUp => T::WeightInfo::renounce_candidacy_runners_up(),
}]
fn renounce_candidacy(origin, renouncing: Renouncing) {
let who = ensure_signed(origin)?;
match renouncing {
Renouncing::Member => {
let _ = Self::remove_and_replace_member(&who, false)
.map_err(|_| Error::<T>::InvalidRenouncing)?;
Self::deposit_event(RawEvent::Renounced(who));
},
Renouncing::RunnerUp => {
<RunnersUp<T>>::try_mutate::<_, Error<T>, _>(|runners_up| {
let index = runners_up
.iter()
.position(|SeatHolder { who: r, .. }| r == &who)
.ok_or(Error::<T>::InvalidRenouncing)?;
let SeatHolder { deposit, .. } = runners_up.remove(index);
let _remainder = T::Currency::unreserve(&who, deposit);
debug_assert!(_remainder.is_zero());
Self::deposit_event(RawEvent::Renounced(who));
Ok(())
})?;
}
Renouncing::Candidate(count) => {
<Candidates<T>>::try_mutate::<_, Error<T>, _>(|candidates| {
ensure!(count >= candidates.len() as u32, Error::<T>::InvalidWitnessData);
let index = candidates
.binary_search_by(|(c, _)| c.cmp(&who))
.map_err(|_| Error::<T>::InvalidRenouncing)?;
let (_removed, deposit) = candidates.remove(index);
let _remainder = T::Currency::unreserve(&who, deposit);
debug_assert!(_remainder.is_zero());
Self::deposit_event(RawEvent::Renounced(who));
Ok(())
})?;
}
};
}
#[weight = if *has_replacement {
T::WeightInfo::remove_member_with_replacement()
} else {
T::BlockWeights::get().max_block
}]
fn remove_member(
origin,
who: <T::Lookup as StaticLookup>::Source,
has_replacement: bool,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
let who = T::Lookup::lookup(who)?;
let will_have_replacement = <RunnersUp<T>>::decode_len().map_or(false, |l| l > 0);
if will_have_replacement != has_replacement {
return Err(Error::<T>::InvalidReplacement.with_weight(
T::WeightInfo::remove_member_wrong_refund()
));
}
let had_replacement = Self::remove_and_replace_member(&who, true)?;
debug_assert_eq!(has_replacement, had_replacement);
Self::deposit_event(RawEvent::MemberKicked(who.clone()));
if !had_replacement {
Self::do_phragmen();
}
Ok(None.into())
}
#[weight = T::WeightInfo::clean_defunct_voters(*_num_voters, *_num_defunct)]
fn clean_defunct_voters(origin, _num_voters: u32, _num_defunct: u32) {
let _ = ensure_root(origin)?;
<Voting<T>>::iter()
.filter(|(_, x)| Self::is_defunct_voter(&x.votes))
.for_each(|(dv, _)| {
Self::do_remove_voter(&dv)
})
}
fn on_initialize(n: T::BlockNumber) -> Weight {
let term_duration = T::TermDuration::get();
if !term_duration.is_zero() && (n % term_duration).is_zero() {
Self::do_phragmen()
} else {
0
}
}
}
}
impl<T: Config> Module<T> {
fn deposit_of(count: usize) -> BalanceOf<T> {
T::VotingBondBase::get().saturating_add(
T::VotingBondFactor::get().saturating_mul((count as u32).into())
)
}
fn remove_and_replace_member(who: &T::AccountId, slash: bool) -> Result<bool, DispatchError> {
let maybe_replacement = <Members<T>>::try_mutate::<_, Error<T>, _>(|members| {
let remove_index =
members.binary_search_by(|m| m.who.cmp(who)).map_err(|_| Error::<T>::NotMember)?;
let removed = members.remove(remove_index);
if slash {
let (imbalance, _remainder) = T::Currency::slash_reserved(who, removed.deposit);
debug_assert!(_remainder.is_zero());
T::LoserCandidate::on_unbalanced(imbalance);
Self::deposit_event(RawEvent::SeatHolderSlashed(who.clone(), removed.deposit));
} else {
T::Currency::unreserve(who, removed.deposit);
}
let maybe_next_best = <RunnersUp<T>>::mutate(|r| r.pop()).map(|next_best| {
if let Err(index) = members.binary_search_by(|m| m.who.cmp(&next_best.who)) {
members.insert(index, next_best.clone());
} else {
fabric_support::debug::error!(
"noble-elections-phragmen: a member seems to also be a runner-up."
);
}
next_best
});
Ok(maybe_next_best)
})?;
let remaining_member_ids_sorted = Self::members()
.into_iter()
.map(|x| x.who.clone())
.collect::<Vec<_>>();
let outgoing = &[who.clone()];
let maybe_current_prime = T::ChangeMembers::get_prime();
let return_value = match maybe_replacement {
Some(incoming) => {
T::ChangeMembers::change_members_sorted(
&[incoming.who],
outgoing,
&remaining_member_ids_sorted[..]
);
true
}
None => {
T::ChangeMembers::change_members_sorted(
&[],
outgoing,
&remaining_member_ids_sorted[..]
);
false
}
};
if let Some(current_prime) = maybe_current_prime {
if ¤t_prime != who {
T::ChangeMembers::set_prime(Some(current_prime));
}
}
Ok(return_value)
}
fn is_candidate(who: &T::AccountId) -> Result<(), usize> {
Self::candidates().binary_search_by(|c| c.0.cmp(who)).map(|_| ())
}
fn is_voter(who: &T::AccountId) -> bool {
Voting::<T>::contains_key(who)
}
fn is_member(who: &T::AccountId) -> bool {
Self::members().binary_search_by(|m| m.who.cmp(who)).is_ok()
}
fn is_runner_up(who: &T::AccountId) -> bool {
Self::runners_up().iter().position(|r| &r.who == who).is_some()
}
fn members_ids() -> Vec<T::AccountId> {
Self::members().into_iter().map(|m| m.who).collect::<Vec<T::AccountId>>()
}
fn implicit_candidates_with_deposit() -> Vec<(T::AccountId, BalanceOf<T>)> {
Self::members()
.into_iter()
.map(|m| (m.who, m.deposit))
.chain(Self::runners_up().into_iter().map(|r| (r.who, r.deposit)))
.collect::<Vec<_>>()
}
fn is_defunct_voter(votes: &[T::AccountId]) -> bool {
votes.iter().all(|v|
!Self::is_member(v) &&
!Self::is_runner_up(v) &&
!Self::is_candidate(v).is_ok()
)
}
fn do_remove_voter(who: &T::AccountId) {
let Voter { deposit, .. } = <Voting<T>>::take(who);
T::Currency::remove_lock(T::ModuleId::get(), who);
let _remainder = T::Currency::unreserve(who, deposit);
debug_assert!(_remainder.is_zero());
}
fn do_phragmen() -> Weight {
let desired_seats = T::DesiredMembers::get() as usize;
let desired_runners_up = T::DesiredRunnersUp::get() as usize;
let num_to_elect = desired_runners_up + desired_seats;
let mut candidates_and_deposit = Self::candidates();
candidates_and_deposit.append(&mut Self::implicit_candidates_with_deposit());
if candidates_and_deposit.len().is_zero() {
Self::deposit_event(RawEvent::EmptyTerm);
return T::DbWeight::get().reads(5);
}
let candidate_ids = candidates_and_deposit
.iter()
.map(|(x, _)| x)
.cloned()
.collect::<Vec<_>>();
let total_issuance = T::Currency::total_issuance();
let to_votes = |b: BalanceOf<T>| T::CurrencyToVote::to_vote(b, total_issuance);
let to_balance = |e: ExtendedBalance| T::CurrencyToVote::to_currency(e, total_issuance);
let mut num_edges: u32 = 0;
let voters_and_stakes = Voting::<T>::iter()
.map(|(voter, Voter { stake, votes, .. })| { (voter, stake, votes) })
.collect::<Vec<_>>();
let voters_and_votes = voters_and_stakes.iter()
.cloned()
.map(|(voter, stake, votes)| {
num_edges = num_edges.saturating_add(votes.len() as u32);
(voter, to_votes(stake), votes)
})
.collect::<Vec<_>>();
let weight_candidates = candidates_and_deposit.len() as u32;
let weight_voters = voters_and_votes.len() as u32;
let weight_edges = num_edges;
let _ = tp_npos_elections::seq_phragmen::<T::AccountId, Perbill>(
num_to_elect,
candidate_ids,
voters_and_votes.clone(),
None,
).map(|ElectionResult { winners, assignments: _, }| {
let old_members_ids_sorted = <Members<T>>::take().into_iter()
.map(|m| m.who)
.collect::<Vec<T::AccountId>>();
let mut old_runners_up_ids_sorted = <RunnersUp<T>>::take().into_iter()
.map(|r| r.who)
.collect::<Vec<T::AccountId>>();
old_runners_up_ids_sorted.sort();
let mut new_set_with_stake = winners
.into_iter()
.filter_map(|(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) })
.collect::<Vec<(T::AccountId, BalanceOf<T>)>>();
let split_point = desired_seats.min(new_set_with_stake.len());
let mut new_members_sorted_by_id = new_set_with_stake.drain(..split_point).collect::<Vec<_>>();
new_members_sorted_by_id.sort_by(|i, j| i.0.cmp(&j.0));
new_set_with_stake.reverse();
let new_runners_up_sorted_by_rank = new_set_with_stake;
let mut new_runners_up_ids_sorted = new_runners_up_sorted_by_rank
.iter()
.map(|(r, _)| r.clone())
.collect::<Vec<_>>();
new_runners_up_ids_sorted.sort();
let mut prime_votes = new_members_sorted_by_id
.iter()
.map(|c| (&c.0, BalanceOf::<T>::zero()))
.collect::<Vec<_>>();
for (_, stake, votes) in voters_and_stakes.into_iter() {
for (vote_multiplier, who) in votes.iter()
.enumerate()
.map(|(vote_position, who)| ((MAXIMUM_VOTE - vote_position) as u32, who))
{
if let Ok(i) = prime_votes.binary_search_by_key(&who, |k| k.0) {
prime_votes[i].1 = prime_votes[i].1.saturating_add(
stake.saturating_mul(vote_multiplier.into())
);
}
}
}
let prime = prime_votes.into_iter().max_by_key(|x| x.1).map(|x| x.0.clone());
let new_members_ids_sorted = new_members_sorted_by_id
.iter()
.map(|(m, _)| m.clone())
.collect::<Vec<T::AccountId>>();
let (incoming, outgoing) = T::ChangeMembers::compute_members_diff_sorted(
&new_members_ids_sorted,
&old_members_ids_sorted,
);
T::ChangeMembers::change_members_sorted(
&incoming,
&outgoing,
&new_members_ids_sorted,
);
T::ChangeMembers::set_prime(prime);
candidates_and_deposit.iter().for_each(|(c, d)| {
if
new_members_ids_sorted.binary_search(c).is_err() &&
new_runners_up_ids_sorted.binary_search(c).is_err()
{
let (imbalance, _) = T::Currency::slash_reserved(c, *d);
T::LoserCandidate::on_unbalanced(imbalance);
Self::deposit_event(RawEvent::CandidateSlashed(c.clone(), *d));
}
});
let deposit_of_candidate = |x: &T::AccountId| -> BalanceOf<T> {
candidates_and_deposit
.iter()
.find_map(|(c, d)| if c == x { Some(*d) } else { None })
.unwrap_or_default()
};
<Members<T>>::put(
new_members_sorted_by_id
.iter()
.map(|(who, stake)| SeatHolder {
deposit: deposit_of_candidate(&who),
who: who.clone(),
stake: stake.clone(),
})
.collect::<Vec<_>>(),
);
<RunnersUp<T>>::put(
new_runners_up_sorted_by_rank
.into_iter()
.map(|(who, stake)| SeatHolder {
deposit: deposit_of_candidate(&who),
who,
stake,
})
.collect::<Vec<_>>(),
);
<Candidates<T>>::kill();
Self::deposit_event(RawEvent::NewTerm(new_members_sorted_by_id));
ElectionRounds::mutate(|v| *v += 1);
}).map_err(|e| {
fabric_support::debug::error!("elections-phragmen: failed to run election [{:?}].", e);
Self::deposit_event(RawEvent::ElectionError);
});
T::WeightInfo::election_phragmen(weight_candidates, weight_voters, weight_edges)
}
}
impl<T: Config> Contains<T::AccountId> for Module<T> {
fn contains(who: &T::AccountId) -> bool {
Self::is_member(who)
}
fn sorted_members() -> Vec<T::AccountId> {
Self::members_ids()
}
#[cfg(feature = "runtime-benchmarks")]
fn add(who: &T::AccountId) {
Members::<T>::mutate(|members| {
match members.binary_search_by(|m| m.who.cmp(who)) {
Ok(_) => (),
Err(pos) => members.insert(pos, SeatHolder { who: who.clone(), ..Default::default() }),
}
})
}
}
impl<T: Config> ContainsLengthBound for Module<T> {
fn min_len() -> usize { 0 }
fn max_len() -> usize {
T::DesiredMembers::get() as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
use fabric_support::{assert_ok, assert_noop, parameter_types,
traits::OnInitialize,
};
use tetcore_test_utils::assert_eq_uvec;
use tet_core::H256;
use tp_runtime::{
testing::Header, BuildStorage, DispatchResult,
traits::{BlakeTwo256, IdentityLookup},
};
use crate as elections_phragmen;
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub BlockWeights: fabric_system::limits::BlockWeights =
fabric_system::limits::BlockWeights::simple_max(1024);
}
impl fabric_system::Config for Test {
type BaseCallFilter = ();
type BlockWeights = BlockWeights;
type BlockLength = ();
type DbWeight = ();
type Origin = Origin;
type Index = u64;
type BlockNumber = u64;
type Call = Call;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = Event;
type BlockHashCount = BlockHashCount;
type Version = ();
type NobleInfo = ();
type AccountData = noble_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
}
parameter_types! {
pub const ExistentialDeposit: u64 = 1;
}
impl noble_balances::Config for Test {
type Balance = u64;
type Event = Event;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = fabric_system::Module<Test>;
type MaxLocks = ();
type WeightInfo = ();
}
fabric_support::parameter_types! {
pub static VotingBondBase: u64 = 2;
pub static VotingBondFactor: u64 = 0;
pub static CandidacyBond: u64 = 3;
pub static DesiredMembers: u32 = 2;
pub static DesiredRunnersUp: u32 = 0;
pub static TermDuration: u64 = 5;
pub static Members: Vec<u64> = vec![];
pub static Prime: Option<u64> = None;
}
pub struct TestChangeMembers;
impl ChangeMembers<u64> for TestChangeMembers {
fn change_members_sorted(incoming: &[u64], outgoing: &[u64], new: &[u64]) {
let mut new_sorted = new.to_vec();
new_sorted.sort();
assert_eq!(new, &new_sorted[..]);
let mut incoming_sorted = incoming.to_vec();
incoming_sorted.sort();
assert_eq!(incoming, &incoming_sorted[..]);
let mut outgoing_sorted = outgoing.to_vec();
outgoing_sorted.sort();
assert_eq!(outgoing, &outgoing_sorted[..]);
for x in incoming.iter() {
assert!(outgoing.binary_search(x).is_err());
}
let mut old_plus_incoming = MEMBERS.with(|m| m.borrow().to_vec());
old_plus_incoming.extend_from_slice(incoming);
old_plus_incoming.sort();
let mut new_plus_outgoing = new.to_vec();
new_plus_outgoing.extend_from_slice(outgoing);
new_plus_outgoing.sort();
assert_eq!(old_plus_incoming, new_plus_outgoing, "change members call is incorrect!");
MEMBERS.with(|m| *m.borrow_mut() = new.to_vec());
PRIME.with(|p| *p.borrow_mut() = None);
}
fn set_prime(who: Option<u64>) {
PRIME.with(|p| *p.borrow_mut() = who);
}
fn get_prime() -> Option<u64> {
PRIME.with(|p| *p.borrow())
}
}
parameter_types! {
pub const ElectionsPhragmenModuleId: LockIdentifier = *b"phrelect";
}
impl Config for Test {
type ModuleId = ElectionsPhragmenModuleId;
type Event = Event;
type Currency = Balances;
type CurrencyToVote = fabric_support::traits::SaturatingCurrencyToVote;
type ChangeMembers = TestChangeMembers;
type InitializeMembers = ();
type CandidacyBond = CandidacyBond;
type VotingBondBase = VotingBondBase;
type VotingBondFactor = VotingBondFactor;
type TermDuration = TermDuration;
type DesiredMembers = DesiredMembers;
type DesiredRunnersUp = DesiredRunnersUp;
type LoserCandidate = ();
type KickedMember = ();
type WeightInfo = ();
}
pub type Block = tp_runtime::generic::Block<Header, UncheckedExtrinsic>;
pub type UncheckedExtrinsic = tp_runtime::generic::UncheckedExtrinsic<u32, u64, Call, ()>;
fabric_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: fabric_system::{Module, Call, Event<T>},
Balances: noble_balances::{Module, Call, Event<T>, Config<T>},
Elections: elections_phragmen::{Module, Call, Event<T>, Config<T>},
}
);
pub struct ExtBuilder {
balance_factor: u64,
genesis_members: Vec<(u64, u64)>,
}
impl Default for ExtBuilder {
fn default() -> Self {
Self {
balance_factor: 1,
genesis_members: vec![],
}
}
}
impl ExtBuilder {
pub fn voter_bond(self, bond: u64) -> Self {
VOTING_BOND_BASE.with(|v| *v.borrow_mut() = bond);
self
}
pub fn voter_bond_factor(self, bond: u64) -> Self {
VOTING_BOND_FACTOR.with(|v| *v.borrow_mut() = bond);
self
}
pub fn desired_runners_up(self, count: u32) -> Self {
DESIRED_RUNNERS_UP.with(|v| *v.borrow_mut() = count);
self
}
pub fn term_duration(self, duration: u64) -> Self {
TERM_DURATION.with(|v| *v.borrow_mut() = duration);
self
}
pub fn genesis_members(mut self, members: Vec<(u64, u64)>) -> Self {
MEMBERS.with(|m| {
*m.borrow_mut() = members
.iter()
.map(|(m, _)| m.clone())
.collect::<Vec<_>>()
});
self.genesis_members = members;
self
}
pub fn desired_members(self, count: u32) -> Self {
DESIRED_MEMBERS.with(|m| *m.borrow_mut() = count);
self
}
pub fn balance_factor(mut self, factor: u64) -> Self {
self.balance_factor = factor;
self
}
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
MEMBERS.with(|m| *m.borrow_mut() = self.genesis_members.iter().map(|(m, _)| m.clone()).collect::<Vec<_>>());
let mut ext: tet_io::TestExternalities = GenesisConfig {
noble_balances: Some(noble_balances::GenesisConfig::<Test>{
balances: vec![
(1, 10 * self.balance_factor),
(2, 20 * self.balance_factor),
(3, 30 * self.balance_factor),
(4, 40 * self.balance_factor),
(5, 50 * self.balance_factor),
(6, 60 * self.balance_factor)
],
}),
elections_phragmen: Some(elections_phragmen::GenesisConfig::<Test> {
members: self.genesis_members
}),
}.build_storage().unwrap().into();
ext.execute_with(pre_conditions);
ext.execute_with(test);
ext.execute_with(post_conditions)
}
}
fn candidate_ids() -> Vec<u64> {
Elections::candidates()
.into_iter()
.map(|(c, _)| c)
.collect::<Vec<_>>()
}
fn candidate_deposit(who: &u64) -> u64 {
Elections::candidates()
.into_iter()
.find_map(|(c, d)| if c == *who { Some(d) } else { None })
.unwrap_or_default()
}
fn voter_deposit(who: &u64) -> u64 {
Elections::voting(who).deposit
}
fn runners_up_ids() -> Vec<u64> {
Elections::runners_up().into_iter().map(|r| r.who).collect::<Vec<_>>()
}
fn members_ids() -> Vec<u64> {
Elections::members_ids()
}
fn members_and_stake() -> Vec<(u64, u64)> {
Elections::members().into_iter().map(|m| (m.who, m.stake)).collect::<Vec<_>>()
}
fn runners_up_and_stake() -> Vec<(u64, u64)> {
Elections::runners_up().into_iter().map(|r| (r.who, r.stake)).collect::<Vec<_>>()
}
fn all_voters() -> Vec<u64> {
Voting::<Test>::iter().map(|(v, _)| v).collect::<Vec<u64>>()
}
fn balances(who: &u64) -> (u64, u64) {
(Balances::free_balance(who), Balances::reserved_balance(who))
}
fn has_lock(who: &u64) -> u64 {
dbg!(Balances::locks(who));
Balances::locks(who)
.get(0)
.cloned()
.map(|lock| {
assert_eq!(lock.id, ElectionsPhragmenModuleId::get());
lock.amount
})
.unwrap_or_default()
}
fn intersects<T: PartialEq>(a: &[T], b: &[T]) -> bool {
a.iter().any(|e| b.contains(e))
}
fn ensure_members_sorted() {
let mut members = Elections::members().clone();
members.sort_by_key(|m| m.who);
assert_eq!(Elections::members(), members);
}
fn ensure_candidates_sorted() {
let mut candidates = Elections::candidates().clone();
candidates.sort_by_key(|(c, _)| *c);
assert_eq!(Elections::candidates(), candidates);
}
fn locked_stake_of(who: &u64) -> u64 {
Voting::<Test>::get(who).stake
}
fn ensure_members_has_approval_stake() {
assert!(Elections::members()
.iter()
.chain(Elections::runners_up().iter())
.all(|s| s.stake != u64::zero()));
}
fn ensure_member_candidates_runners_up_disjoint() {
assert!(!intersects(&members_ids(), &candidate_ids()));
assert!(!intersects(&members_ids(), &runners_up_ids()));
assert!(!intersects(&candidate_ids(), &runners_up_ids()));
}
fn pre_conditions() {
System::set_block_number(1);
ensure_members_sorted();
ensure_candidates_sorted();
ensure_member_candidates_runners_up_disjoint();
}
fn post_conditions() {
ensure_members_sorted();
ensure_candidates_sorted();
ensure_member_candidates_runners_up_disjoint();
ensure_members_has_approval_stake();
}
fn submit_candidacy(origin: Origin) -> DispatchResult {
Elections::submit_candidacy(origin, Elections::candidates().len() as u32)
}
fn vote(origin: Origin, votes: Vec<u64>, stake: u64) -> DispatchResult {
ensure_signed(origin.clone()).expect("vote origin must be signed");
Elections::vote(origin, votes, stake)
}
fn votes_of(who: &u64) -> Vec<u64> {
Voting::<Test>::get(who).votes
}
#[test]
fn params_should_work() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(<Test as Config>::DesiredMembers::get(), 2);
assert_eq!(<Test as Config>::DesiredRunnersUp::get(), 0);
assert_eq!(<Test as Config>::VotingBondBase::get(), 2);
assert_eq!(<Test as Config>::VotingBondFactor::get(), 0);
assert_eq!(<Test as Config>::CandidacyBond::get(), 3);
assert_eq!(<Test as Config>::TermDuration::get(), 5);
assert_eq!(Elections::election_rounds(), 0);
assert!(Elections::members().is_empty());
assert!(Elections::runners_up().is_empty());
assert!(candidate_ids().is_empty());
assert_eq!(<Candidates<Test>>::decode_len(), None);
assert!(Elections::is_candidate(&1).is_err());
assert!(all_voters().is_empty());
assert!(votes_of(&1).is_empty());
});
}
#[test]
fn genesis_members_should_work() {
ExtBuilder::default().genesis_members(vec![(1, 10), (2, 20)]).build_and_execute(|| {
System::set_block_number(1);
assert_eq!(
Elections::members(),
vec![
SeatHolder { who: 1, stake: 10, deposit: 0 },
SeatHolder { who: 2, stake: 20, deposit: 0 }
]
);
assert_eq!(Elections::voting(1), Voter { stake: 10u64, votes: vec![1], deposit: 0 });
assert_eq!(Elections::voting(2), Voter { stake: 20u64, votes: vec![2], deposit: 0 });
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![1, 2]);
})
}
#[test]
fn genesis_voters_can_remove_lock() {
ExtBuilder::default().genesis_members(vec![(1, 10), (2, 20)]).build_and_execute(|| {
System::set_block_number(1);
assert_eq!(Elections::voting(1), Voter { stake: 10u64, votes: vec![1], deposit: 0 });
assert_eq!(Elections::voting(2), Voter { stake: 20u64, votes: vec![2], deposit: 0 });
assert_ok!(Elections::remove_voter(Origin::signed(1)));
assert_ok!(Elections::remove_voter(Origin::signed(2)));
assert_eq!(Elections::voting(1), Default::default());
assert_eq!(Elections::voting(2), Default::default());
})
}
#[test]
fn genesis_members_unsorted_should_work() {
ExtBuilder::default().genesis_members(vec![(2, 20), (1, 10)]).build_and_execute(|| {
System::set_block_number(1);
assert_eq!(
Elections::members(),
vec![
SeatHolder { who: 1, stake: 10, deposit: 0 },
SeatHolder { who: 2, stake: 20, deposit: 0 },
]
);
assert_eq!(Elections::voting(1), Voter { stake: 10u64, votes: vec![1], deposit: 0 });
assert_eq!(Elections::voting(2), Voter { stake: 20u64, votes: vec![2], deposit: 0 });
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![1, 2]);
})
}
#[test]
#[should_panic = "Genesis member does not have enough stake"]
fn genesis_members_cannot_over_stake_0() {
ExtBuilder::default()
.genesis_members(vec![(1, 20), (2, 20)])
.build_and_execute(|| {});
}
#[test]
#[should_panic = "Duplicate member in elections-phragmen genesis: 2"]
fn genesis_members_cannot_be_duplicate() {
ExtBuilder::default()
.desired_members(3)
.genesis_members(vec![(1, 10), (2, 10), (2, 10)])
.build_and_execute(|| {});
}
#[test]
#[should_panic = "Cannot accept more than DesiredMembers genesis member"]
fn genesis_members_cannot_too_many() {
ExtBuilder::default()
.genesis_members(vec![(1, 10), (2, 10), (3, 30)])
.desired_members(2)
.build_and_execute(|| {});
}
#[test]
fn term_duration_zero_is_passive() {
ExtBuilder::default()
.term_duration(0)
.build_and_execute(||
{
assert_eq!(<Test as Config>::TermDuration::get(), 0);
assert_eq!(<Test as Config>::DesiredMembers::get(), 2);
assert_eq!(Elections::election_rounds(), 0);
assert!(members_ids().is_empty());
assert!(Elections::runners_up().is_empty());
assert!(candidate_ids().is_empty());
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert!(members_ids().is_empty());
assert!(Elections::runners_up().is_empty());
assert!(candidate_ids().is_empty());
});
}
#[test]
fn simple_candidate_submission_should_work() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(candidate_ids(), Vec::<u64>::new());
assert!(Elections::is_candidate(&1).is_err());
assert!(Elections::is_candidate(&2).is_err());
assert_eq!(balances(&1), (10, 0));
assert_ok!(submit_candidacy(Origin::signed(1)));
assert_eq!(balances(&1), (7, 3));
assert_eq!(candidate_ids(), vec![1]);
assert!(Elections::is_candidate(&1).is_ok());
assert!(Elections::is_candidate(&2).is_err());
assert_eq!(balances(&2), (20, 0));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_eq!(balances(&2), (17, 3));
assert_eq!(candidate_ids(), vec![1, 2]);
assert!(Elections::is_candidate(&1).is_ok());
assert!(Elections::is_candidate(&2).is_ok());
assert_eq!(candidate_deposit(&1), 3);
assert_eq!(candidate_deposit(&2), 3);
assert_eq!(candidate_deposit(&3), 0);
});
}
#[test]
fn updating_candidacy_bond_works() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_eq!(Elections::candidates(), vec![(5, 3)]);
CANDIDACY_BOND.with(|v| *v.borrow_mut() = 4);
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_eq!(Elections::candidates(), vec![(4, 4), (5, 3)]);
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(
Elections::members(),
vec![
SeatHolder { who: 4, stake: 40, deposit: 4 },
SeatHolder { who: 5, stake: 50, deposit: 3 },
]
);
})
}
#[test]
fn candidates_are_always_sorted() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(candidate_ids(), Vec::<u64>::new());
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_eq!(candidate_ids(), vec![3]);
assert_ok!(submit_candidacy(Origin::signed(1)));
assert_eq!(candidate_ids(), vec![1, 3]);
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_eq!(candidate_ids(), vec![1, 2, 3]);
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_eq!(candidate_ids(), vec![1, 2, 3, 4]);
});
}
#[test]
fn dupe_candidate_submission_should_not_work() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(candidate_ids(), Vec::<u64>::new());
assert_ok!(submit_candidacy(Origin::signed(1)));
assert_eq!(candidate_ids(), vec![1]);
assert_noop!(
submit_candidacy(Origin::signed(1)),
Error::<Test>::DuplicatedCandidate,
);
});
}
#[test]
fn member_candidacy_submission_should_not_work() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![5]);
assert!(Elections::runners_up().is_empty());
assert!(candidate_ids().is_empty());
assert_noop!(
submit_candidacy(Origin::signed(5)),
Error::<Test>::MemberSubmit,
);
});
}
#[test]
fn runner_candidate_submission_should_not_work() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(2), vec![5, 4], 20));
assert_ok!(vote(Origin::signed(1), vec![3], 10));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![3]);
assert_noop!(
submit_candidacy(Origin::signed(3)),
Error::<Test>::RunnerUpSubmit,
);
});
}
#[test]
fn poor_candidate_submission_should_not_work() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(candidate_ids(), Vec::<u64>::new());
assert_noop!(
submit_candidacy(Origin::signed(7)),
Error::<Test>::InsufficientCandidateFunds,
);
});
}
#[test]
fn simple_voting_should_work() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(candidate_ids(), Vec::<u64>::new());
assert_eq!(balances(&2), (20, 0));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_eq!(balances(&2), (18, 2));
assert_eq!(has_lock(&2), 20);
});
}
#[test]
fn can_vote_with_custom_stake() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(candidate_ids(), Vec::<u64>::new());
assert_eq!(balances(&2), (20, 0));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(2), vec![5], 12));
assert_eq!(balances(&2), (18, 2));
assert_eq!(has_lock(&2), 12);
});
}
#[test]
fn can_update_votes_and_stake() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(balances(&2), (20, 0));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_eq!(balances(&2), (18, 2));
assert_eq!(has_lock(&2), 20);
assert_eq!(locked_stake_of(&2), 20);
assert_ok!(vote(Origin::signed(2), vec![5, 4], 15));
assert_eq!(balances(&2), (18, 2));
assert_eq!(has_lock(&2), 15);
assert_eq!(locked_stake_of(&2), 15);
});
}
#[test]
fn updated_voting_bond_works() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_eq!(balances(&2), (20, 0));
assert_ok!(vote(Origin::signed(2), vec![5], 5));
assert_eq!(balances(&2), (18, 2));
assert_eq!(voter_deposit(&2), 2);
VOTING_BOND_BASE.with(|v| *v.borrow_mut() = 1);
assert_eq!(balances(&1), (10, 0));
assert_ok!(vote(Origin::signed(1), vec![5], 5));
assert_eq!(balances(&1), (9, 1));
assert_eq!(voter_deposit(&1), 1);
assert_ok!(Elections::remove_voter(Origin::signed(2)));
assert_eq!(balances(&2), (20, 0));
})
}
#[test]
fn voting_reserves_bond_per_vote() {
ExtBuilder::default().voter_bond_factor(1).build_and_execute(|| {
assert_eq!(balances(&2), (20, 0));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(2), vec![5], 10));
assert_eq!(balances(&2), (17, 3));
assert_eq!(Elections::voting(&2).deposit, 3);
assert_eq!(has_lock(&2), 10);
assert_eq!(locked_stake_of(&2), 10);
assert_ok!(vote(Origin::signed(2), vec![5, 4], 15));
assert_eq!(balances(&2), (16, 4));
assert_eq!(Elections::voting(&2).deposit, 4);
assert_eq!(has_lock(&2), 15);
assert_eq!(locked_stake_of(&2), 15);
assert_ok!(vote(Origin::signed(2), vec![5, 3], 18));
assert_eq!(balances(&2), (16, 4));
assert_eq!(Elections::voting(&2).deposit, 4);
assert_eq!(has_lock(&2), 18);
assert_eq!(locked_stake_of(&2), 18);
assert_ok!(vote(Origin::signed(2), vec![4], 12));
assert_eq!(balances(&2), (17, 3));
assert_eq!(Elections::voting(&2).deposit, 3);
assert_eq!(has_lock(&2), 12);
assert_eq!(locked_stake_of(&2), 12);
});
}
#[test]
fn cannot_vote_for_no_candidate() {
ExtBuilder::default().build_and_execute(|| {
assert_noop!(
vote(Origin::signed(2), vec![], 20),
Error::<Test>::NoVotes,
);
});
}
#[test]
fn can_vote_for_old_members_even_when_no_new_candidates() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(2), vec![4, 5], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert!(candidate_ids().is_empty());
assert_ok!(vote(Origin::signed(3), vec![4, 5], 10));
});
}
#[test]
fn prime_works() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(1), vec![4, 3], 10));
assert_ok!(vote(Origin::signed(2), vec![4], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert!(candidate_ids().is_empty());
assert_ok!(vote(Origin::signed(3), vec![4, 5], 10));
assert_eq!(PRIME.with(|p| *p.borrow()), Some(4));
});
}
#[test]
fn prime_votes_for_exiting_members_are_removed() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(1), vec![4, 3], 10));
assert_ok!(vote(Origin::signed(2), vec![4], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(3)));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![3, 5]);
assert!(candidate_ids().is_empty());
assert_eq!(PRIME.with(|p| *p.borrow()), Some(5));
});
}
#[test]
fn prime_is_kept_if_other_members_leave() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(PRIME.with(|p| *p.borrow()), Some(5));
assert_ok!(Elections::renounce_candidacy(
Origin::signed(4),
Renouncing::Member
));
assert_eq!(members_ids(), vec![5]);
assert_eq!(PRIME.with(|p| *p.borrow()), Some(5));
})
}
#[test]
fn prime_is_gone_if_renouncing() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(PRIME.with(|p| *p.borrow()), Some(5));
assert_ok!(Elections::renounce_candidacy(Origin::signed(5), Renouncing::Member));
assert_eq!(members_ids(), vec![4]);
assert_eq!(PRIME.with(|p| *p.borrow()), None);
})
}
#[test]
fn cannot_vote_for_more_than_candidates_and_members_and_runners() {
ExtBuilder::default()
.desired_runners_up(1)
.balance_factor(10)
.build_and_execute(
|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_noop!(
vote(Origin::signed(1), vec![9, 99, 999, 9999], 5),
Error::<Test>::TooManyVotes,
);
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(1), vec![9, 99, 999, 9999], 5));
assert_noop!(
vote(Origin::signed(1), vec![9, 99, 999, 9_999, 99_999], 5),
Error::<Test>::TooManyVotes,
);
});
}
#[test]
fn cannot_vote_for_less_than_ed() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_noop!(
vote(Origin::signed(2), vec![4], 1),
Error::<Test>::LowBalance,
);
})
}
#[test]
fn can_vote_for_more_than_total_balance_but_moot() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(2), vec![4, 5], 30));
assert_eq!(locked_stake_of(&2), 20);
assert_eq!(has_lock(&2), 20);
});
}
#[test]
fn remove_voter_should_work() {
ExtBuilder::default().voter_bond(8).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_ok!(vote(Origin::signed(3), vec![5], 30));
assert_eq_uvec!(all_voters(), vec![2, 3]);
assert_eq!(locked_stake_of(&2), 20);
assert_eq!(locked_stake_of(&3), 30);
assert_eq!(votes_of(&2), vec![5]);
assert_eq!(votes_of(&3), vec![5]);
assert_ok!(Elections::remove_voter(Origin::signed(2)));
assert_eq_uvec!(all_voters(), vec![3]);
assert!(votes_of(&2).is_empty());
assert_eq!(locked_stake_of(&2), 0);
assert_eq!(balances(&2), (20, 0));
assert_eq!(Balances::locks(&2).len(), 0);
});
}
#[test]
fn non_voter_remove_should_not_work() {
ExtBuilder::default().build_and_execute(|| {
assert_noop!(Elections::remove_voter(Origin::signed(3)), Error::<Test>::MustBeVoter);
});
}
#[test]
fn dupe_remove_should_fail() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_ok!(Elections::remove_voter(Origin::signed(2)));
assert!(all_voters().is_empty());
assert_noop!(Elections::remove_voter(Origin::signed(2)), Error::<Test>::MustBeVoter);
});
}
#[test]
fn removed_voter_should_not_be_counted() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(Elections::remove_voter(Origin::signed(4)));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![3, 5]);
});
}
#[test]
fn simple_voting_rounds_should_work() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_ok!(vote(Origin::signed(4), vec![4], 15));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_eq_uvec!(all_voters(), vec![2, 3, 4]);
assert_eq!(votes_of(&2), vec![5]);
assert_eq!(votes_of(&3), vec![3]);
assert_eq!(votes_of(&4), vec![4]);
assert_eq!(candidate_ids(), vec![3, 4, 5]);
assert_eq!(<Candidates<Test>>::decode_len().unwrap(), 3);
assert_eq!(Elections::election_rounds(), 0);
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(3, 30), (5, 20)]);
assert!(Elections::runners_up().is_empty());
assert_eq_uvec!(all_voters(), vec![2, 3, 4]);
assert!(candidate_ids().is_empty());
assert_eq!(<Candidates<Test>>::decode_len(), None);
assert_eq!(Elections::election_rounds(), 1);
});
}
#[test]
fn empty_term() {
ExtBuilder::default().build_and_execute(|| {
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(
System::events().iter().last().unwrap().event,
Event::elections_phragmen(RawEvent::EmptyTerm),
)
})
}
#[test]
fn all_outgoing() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(
System::events().iter().last().unwrap().event,
Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)])),
);
assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]);
assert_eq!(runners_up_and_stake(), vec![]);
assert_ok!(Elections::remove_voter(Origin::signed(5)));
assert_ok!(Elections::remove_voter(Origin::signed(4)));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(
System::events().iter().last().unwrap().event,
Event::elections_phragmen(RawEvent::NewTerm(vec![])),
);
assert_eq!(balances(&4), (37, 0));
assert_eq!(balances(&5), (47, 0));
});
}
#[test]
fn defunct_voter_will_be_counted() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(3), vec![4], 30));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(5, 50)]);
assert_eq!(Elections::election_rounds(), 1);
assert_ok!(submit_candidacy(Origin::signed(4)));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(4, 30), (5, 50)]);
assert_eq!(Elections::election_rounds(), 2);
assert_eq_uvec!(all_voters(), vec![3, 5]);
});
}
#[test]
fn only_desired_seats_are_chosen() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(Elections::election_rounds(), 1);
assert_eq!(members_ids(), vec![4, 5]);
});
}
#[test]
fn phragmen_should_not_self_vote() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert!(candidate_ids().is_empty());
assert_eq!(Elections::election_rounds(), 1);
assert!(members_ids().is_empty());
assert_eq!(
System::events().iter().last().unwrap().event,
Event::elections_phragmen(RawEvent::NewTerm(vec![])),
)
});
}
#[test]
fn runners_up_should_be_kept() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![3], 20));
assert_ok!(vote(Origin::signed(3), vec![2], 30));
assert_ok!(vote(Origin::signed(4), vec![5], 40));
assert_ok!(vote(Origin::signed(5), vec![4], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![3, 2]);
assert_eq!(balances(&4), (35, 5));
assert_eq!(balances(&5), (45, 5));
assert_eq!(balances(&3), (25, 5));
});
}
#[test]
fn runners_up_should_be_next_candidates() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]);
assert_eq!(runners_up_and_stake(), vec![(2, 20), (3, 30)]);
assert_ok!(vote(Origin::signed(5), vec![5], 15));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(3, 30), (4, 40)]);
assert_eq!(runners_up_and_stake(), vec![(5, 15), (2, 20)]);
});
}
#[test]
fn runners_up_lose_bond_once_outgoing() {
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![2]);
assert_eq!(balances(&2), (15, 5));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(runners_up_ids(), vec![3]);
assert_eq!(balances(&2), (15, 2));
});
}
#[test]
fn members_lose_bond_once_outgoing() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(balances(&5), (50, 0));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_eq!(balances(&5), (47, 3));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_eq!(balances(&5), (45, 5));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![5]);
assert_ok!(Elections::remove_voter(Origin::signed(5)));
assert_eq!(balances(&5), (47, 3));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert!(members_ids().is_empty());
assert_eq!(balances(&5), (47, 0));
});
}
#[test]
fn candidates_lose_the_bond_when_outgoing() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(4), vec![5], 40));
assert_eq!(balances(&5), (47, 3));
assert_eq!(balances(&3), (27, 3));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![5]);
assert_eq!(balances(&5), (47, 3));
assert_eq!(balances(&3), (27, 0));
});
}
#[test]
fn current_members_are_always_next_candidate() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(Elections::election_rounds(), 1);
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(Elections::remove_voter(Origin::signed(4)));
assert_eq!(candidate_ids(), vec![2, 3]);
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![3, 5]);
});
}
#[test]
fn election_state_is_uninterrupted() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
let check_at_block = |b: u32| {
System::set_block_number(b.into());
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]);
assert_eq!(runners_up_and_stake(), vec![(2, 20), (3, 30)]);
assert!(candidate_ids().is_empty());
assert_eq!(Elections::election_rounds(), b / 5);
assert_eq_uvec!(all_voters(), vec![2, 3, 4, 5]);
};
check_at_block(5);
check_at_block(10);
check_at_block(15);
check_at_block(20);
});
}
#[test]
fn remove_members_triggers_election() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(Elections::election_rounds(), 1);
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(Elections::remove_member(Origin::root(), 4, false));
assert_eq!(balances(&4), (35, 2));
assert_eq!(Elections::election_rounds(), 2);
assert_eq!(members_ids(), vec![3, 5]);
});
}
#[test]
fn remove_member_should_indicate_replacement() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
let unwrapped_error = Elections::remove_member(Origin::root(), 4, true).unwrap_err();
matches!(
unwrapped_error.error,
DispatchError::Module {
message: Some("InvalidReplacement"),
..
}
);
matches!(
unwrapped_error.post_info.actual_weight,
Some(x) if x < <Test as fabric_system::Config>::BlockWeights::get().max_block
);
});
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![3]);
let unwrapped_error = Elections::remove_member(Origin::root(), 4, false).unwrap_err();
matches!(
unwrapped_error.error,
DispatchError::Module {
message: Some("InvalidReplacement"),
..
}
);
matches!(
unwrapped_error.post_info.actual_weight,
Some(x) if x < <Test as fabric_system::Config>::BlockWeights::get().max_block
);
});
}
#[test]
fn seats_should_be_released_when_no_vote() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(2), vec![3], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_eq!(<Candidates<Test>>::decode_len().unwrap(), 3);
assert_eq!(Elections::election_rounds(), 0);
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![3, 5]);
assert_eq!(Elections::election_rounds(), 1);
assert_ok!(Elections::remove_voter(Origin::signed(2)));
assert_ok!(Elections::remove_voter(Origin::signed(3)));
assert_ok!(Elections::remove_voter(Origin::signed(4)));
assert_ok!(Elections::remove_voter(Origin::signed(5)));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert!(members_ids().is_empty());
assert_eq!(Elections::election_rounds(), 2);
});
}
#[test]
fn incoming_outgoing_are_reported() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_ok!(submit_candidacy(Origin::signed(1)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![4], 8));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
assert_ok!(vote(Origin::signed(1), vec![1], 10));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(3, 30), (4, 48)]);
assert_eq!(balances(&3), (25, 5));
assert_eq!(balances(&4), (35, 5));
assert_eq!(balances(&1), (5, 2));
assert_eq!(balances(&5), (45, 2));
assert!(System::events().iter().any(|event| {
event.event == Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)]))
}));
})
}
#[test]
fn invalid_votes_are_moot() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![10], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq_uvec!(members_ids(), vec![3, 4]);
assert_eq!(Elections::election_rounds(), 1);
});
}
#[test]
fn members_are_sorted_based_on_id_runners_on_merit() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![3], 20));
assert_ok!(vote(Origin::signed(3), vec![2], 30));
assert_ok!(vote(Origin::signed(4), vec![5], 40));
assert_ok!(vote(Origin::signed(5), vec![4], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_and_stake(), vec![(4, 50), (5, 40)]);
assert_eq!(runners_up_and_stake(), vec![(3, 20), (2, 30)]);
});
}
#[test]
fn runner_up_replacement_maintains_members_order() {
ExtBuilder::default()
.desired_runners_up(2)
.build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![2], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![2, 4]);
assert_ok!(Elections::remove_member(Origin::root(), 2, true));
assert_eq!(members_ids(), vec![4, 5]);
});
}
#[test]
fn can_renounce_candidacy_member_with_runners_bond_is_refunded() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![2, 3]);
assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Member));
assert_eq!(balances(&4), (38, 2));
assert_eq!(members_ids(), vec![3, 5]);
assert_eq!(runners_up_ids(), vec![2]);
})
}
#[test]
fn can_renounce_candidacy_member_without_runners_bond_is_refunded() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert!(runners_up_ids().is_empty());
assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Member));
assert_eq!(balances(&4), (38, 2));
assert_eq!(members_ids(), vec![5]);
assert!(runners_up_ids().is_empty());
})
}
#[test]
fn can_renounce_candidacy_runner_up() {
ExtBuilder::default()
.desired_runners_up(2)
.build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(5), vec![4], 50));
assert_ok!(vote(Origin::signed(4), vec![5], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![2, 3]);
assert_ok!(Elections::renounce_candidacy(
Origin::signed(3),
Renouncing::RunnerUp
));
assert_eq!(balances(&3), (28, 2));
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![2]);
})
}
#[test]
fn runner_up_replacement_works_when_out_of_order() {
ExtBuilder::default().desired_runners_up(2).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![5], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(5), vec![2], 50));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![2, 4]);
assert_eq!(runners_up_ids(), vec![5, 3]);
assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::RunnerUp));
assert_eq!(members_ids(), vec![2, 4]);
assert_eq!(runners_up_ids(), vec![5]);
});
}
#[test]
fn can_renounce_candidacy_candidate() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_eq!(balances(&5), (47, 3));
assert_eq!(candidate_ids(), vec![5]);
assert_ok!(Elections::renounce_candidacy(Origin::signed(5), Renouncing::Candidate(1)));
assert_eq!(balances(&5), (50, 0));
assert!(candidate_ids().is_empty());
})
}
#[test]
fn wrong_renounce_candidacy_should_fail() {
ExtBuilder::default().build_and_execute(|| {
assert_noop!(
Elections::renounce_candidacy(Origin::signed(5), Renouncing::Candidate(0)),
Error::<Test>::InvalidRenouncing,
);
assert_noop!(
Elections::renounce_candidacy(Origin::signed(5), Renouncing::Member),
Error::<Test>::InvalidRenouncing,
);
assert_noop!(
Elections::renounce_candidacy(Origin::signed(5), Renouncing::RunnerUp),
Error::<Test>::InvalidRenouncing,
);
})
}
#[test]
fn non_member_renounce_member_should_fail() {
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![3]);
assert_noop!(
Elections::renounce_candidacy(Origin::signed(3), Renouncing::Member),
Error::<Test>::InvalidRenouncing,
);
})
}
#[test]
fn non_runner_up_renounce_runner_up_should_fail() {
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![3]);
assert_noop!(
Elections::renounce_candidacy(Origin::signed(4), Renouncing::RunnerUp),
Error::<Test>::InvalidRenouncing,
);
})
}
#[test]
fn wrong_candidate_count_renounce_should_fail() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_noop!(
Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(2)),
Error::<Test>::InvalidWitnessData,
);
assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(3)));
})
}
#[test]
fn renounce_candidacy_count_can_overestimate() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(4)));
})
}
#[test]
fn unsorted_runners_up_are_detected() {
ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 5));
assert_ok!(vote(Origin::signed(3), vec![3], 15));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![5]);
assert_eq!(runners_up_ids(), vec![4, 3]);
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(2), vec![2], 10));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![5]);
assert_eq!(runners_up_ids(), vec![2, 3]);
assert_eq!(balances(&4), (35, 2));
assert_eq!(balances(&3), (25, 5));
})
}
#[test]
fn member_to_runner_up_wont_slash() {
ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4]);
assert_eq!(runners_up_ids(), vec![2, 3]);
assert_eq!(balances(&4), (35, 5));
assert_eq!(balances(&3), (25, 5));
assert_eq!(balances(&2), (15, 5));
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![5]);
assert_eq!(runners_up_ids(), vec![3, 4]);
assert_eq!(balances(&4), (35, 5));
assert_eq!(balances(&3), (25, 5));
assert_eq!(balances(&2), (15, 2));
});
}
#[test]
fn runner_up_to_member_wont_slash() {
ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4]);
assert_eq!(runners_up_ids(), vec![2, 3]);
assert_eq!(balances(&4), (35, 5));
assert_eq!(balances(&3), (25, 5));
assert_eq!(balances(&2), (15, 5));
assert_ok!(vote(Origin::signed(4), vec![2], 40));
assert_ok!(vote(Origin::signed(2), vec![4], 20));
System::set_block_number(10);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![2]);
assert_eq!(runners_up_ids(), vec![4, 3]);
assert_eq!(balances(&2), (15, 5));
assert_eq!(balances(&4), (35, 5));
assert_eq!(balances(&3), (25, 5));
});
}
#[test]
fn remove_and_replace_member_works() {
let setup = || {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![5], 50));
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![4, 5]);
assert_eq!(runners_up_ids(), vec![3]);
};
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
setup();
assert_eq!(Elections::remove_and_replace_member(&4, false), Ok(true));
assert_eq!(members_ids(), vec![3, 5]);
assert_eq!(runners_up_ids().len(), 0);
});
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
setup();
assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::RunnerUp));
assert_eq!(Elections::remove_and_replace_member(&4, false), Ok(false));
assert_eq!(members_ids(), vec![5]);
assert_eq!(runners_up_ids().len(), 0);
});
ExtBuilder::default().desired_runners_up(1).build_and_execute(|| {
setup();
assert!(matches!(Elections::remove_and_replace_member(&2, false), Err(_)));
});
}
#[test]
fn no_desired_members() {
ExtBuilder::default().desired_members(0).desired_runners_up(0).build_and_execute(|| {
assert_eq!(Elections::candidates().len(), 0);
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_eq!(Elections::candidates().len(), 3);
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids().len(), 0);
assert_eq!(runners_up_ids().len(), 0);
assert_eq!(all_voters().len(), 3);
assert_eq!(Elections::candidates().len(), 0);
});
ExtBuilder::default().desired_members(0).desired_runners_up(2).build_and_execute(|| {
assert_eq!(Elections::candidates().len(), 0);
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_eq!(Elections::candidates().len(), 3);
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids().len(), 0);
assert_eq!(runners_up_ids(), vec![3, 4]);
assert_eq!(all_voters().len(), 3);
assert_eq!(Elections::candidates().len(), 0);
});
ExtBuilder::default().desired_members(2).desired_runners_up(0).build_and_execute(|| {
assert_eq!(Elections::candidates().len(), 0);
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_eq!(Elections::candidates().len(), 3);
assert_ok!(vote(Origin::signed(4), vec![4], 40));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
assert_ok!(vote(Origin::signed(2), vec![2], 20));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![3, 4]);
assert_eq!(runners_up_ids().len(), 0);
assert_eq!(all_voters().len(), 3);
assert_eq!(Elections::candidates().len(), 0);
});
}
#[test]
fn dupe_vote_is_moot() {
ExtBuilder::default().desired_members(1).build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(submit_candidacy(Origin::signed(2)));
assert_ok!(submit_candidacy(Origin::signed(1)));
assert_ok!(vote(Origin::signed(1), vec![2, 2, 2, 2], 5));
assert_ok!(vote(Origin::signed(2), vec![2, 2, 2, 2], 20));
assert_ok!(vote(Origin::signed(3), vec![3], 30));
System::set_block_number(5);
Elections::on_initialize(System::block_number());
assert_eq!(members_ids(), vec![3]);
})
}
#[test]
fn remove_defunct_voter_works() {
ExtBuilder::default().build_and_execute(|| {
assert_ok!(submit_candidacy(Origin::signed(5)));
assert_ok!(submit_candidacy(Origin::signed(4)));
assert_ok!(submit_candidacy(Origin::signed(3)));
assert_ok!(vote(Origin::signed(5), vec![5, 4], 5));
assert_ok!(vote(Origin::signed(4), vec![4], 5));
assert_ok!(vote(Origin::signed(3), vec![3], 5));
assert_ok!(vote(Origin::signed(2), vec![3, 4], 5));
assert_ok!(Elections::renounce_candidacy(Origin::signed(5), Renouncing::Candidate(3)));
assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(2)));
assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::Candidate(1)));
assert_ok!(Elections::clean_defunct_voters(Origin::root(), 4, 2));
})
}
}