Skip to content
Snippets Groups Projects
Commit 972be34e authored by Gavin Wood's avatar Gavin Wood Committed by GitHub
Browse files

Add trivial EnsureFounder verifier to society (#4615)


* Add trivial EnsureFounder verifier to society

* Fix potential panic

* Keep founder account around.

* Cleanups

* Fix.

* Fix tests

Co-authored-by: default avatarShawn Tabrizi <shawntabrizi@gmail.com>
parent ee5e8050
Branches
No related merge requests found
...@@ -15,17 +15,17 @@ ...@@ -15,17 +15,17 @@
// along with Substrate. If not, see <http://www.gnu.org/licenses/>. // along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! # Society Module //! # Society Module
//! //!
//! - [`society::Trait`](./trait.Trait.html) //! - [`society::Trait`](./trait.Trait.html)
//! - [`Call`](./enum.Call.html) //! - [`Call`](./enum.Call.html)
//! //!
//! ## Overview //! ## Overview
//! //!
//! The Society module is an economic game which incentivizes users to participate //! The Society module is an economic game which incentivizes users to participate
//! and maintain a membership society. //! and maintain a membership society.
//! //!
//! ### User Types //! ### User Types
//! //!
//! At any point, a user in the society can be one of a: //! At any point, a user in the society can be one of a:
//! * Bidder - A user who has submitted intention of joining the society. //! * Bidder - A user who has submitted intention of joining the society.
//! * Candidate - A user who will be voted on to join the society. //! * Candidate - A user who will be voted on to join the society.
...@@ -33,31 +33,31 @@ ...@@ -33,31 +33,31 @@
//! * Member - A user who is a member of the society. //! * Member - A user who is a member of the society.
//! * Suspended Member - A member of the society who has accumulated too many strikes //! * Suspended Member - A member of the society who has accumulated too many strikes
//! or failed their membership challenge. //! or failed their membership challenge.
//! //!
//! Of the non-suspended members, there is always a: //! Of the non-suspended members, there is always a:
//! * Head - A member who is exempt from suspension. //! * Head - A member who is exempt from suspension.
//! * Defender - A member whose membership is under question and voted on again. //! * Defender - A member whose membership is under question and voted on again.
//! //!
//! Of the non-suspended members of the society, a random set of them are chosen as //! Of the non-suspended members of the society, a random set of them are chosen as
//! "skeptics". The mechanics of skeptics is explained in the //! "skeptics". The mechanics of skeptics is explained in the
//! [member phase](#member-phase) below. //! [member phase](#member-phase) below.
//! //!
//! ### Mechanics //! ### Mechanics
//! //!
//! #### Rewards //! #### Rewards
//! //!
//! Members are incentivized to participate in the society through rewards paid //! Members are incentivized to participate in the society through rewards paid
//! by the Society treasury. These payments have a maturity period that the user //! by the Society treasury. These payments have a maturity period that the user
//! must wait before they are able to access the funds. //! must wait before they are able to access the funds.
//! //!
//! #### Punishments //! #### Punishments
//! //!
//! Members can be punished by slashing the reward payouts that have not been //! Members can be punished by slashing the reward payouts that have not been
//! collected. Additionally, members can accumulate "strikes", and when they //! collected. Additionally, members can accumulate "strikes", and when they
//! reach a max strike limit, they become suspended. //! reach a max strike limit, they become suspended.
//! //!
//! #### Skeptics //! #### Skeptics
//! //!
//! During the voting period, a random set of members are selected as "skeptics". //! During the voting period, a random set of members are selected as "skeptics".
//! These skeptics are expected to vote on the current candidates. If they do not vote, //! These skeptics are expected to vote on the current candidates. If they do not vote,
//! their skeptic status is treated as a rejection vote, the member is deemed //! their skeptic status is treated as a rejection vote, the member is deemed
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
//! assuming no one else votes, the defender always get a free vote on their //! assuming no one else votes, the defender always get a free vote on their
//! own challenge keeping them in the society. The Head member is exempt from the //! own challenge keeping them in the society. The Head member is exempt from the
//! negative outcome of a membership challenge. //! negative outcome of a membership challenge.
//! //!
//! #### Society Treasury //! #### Society Treasury
//! //!
//! The membership society is independently funded by a treasury managed by this //! The membership society is independently funded by a treasury managed by this
...@@ -80,17 +80,17 @@ ...@@ -80,17 +80,17 @@
//! to determine the number of accepted bids. //! to determine the number of accepted bids.
//! //!
//! #### Rate of Growth //! #### Rate of Growth
//! //!
//! The membership society can grow at a rate of 10 accepted candidates per rotation period up //! The membership society can grow at a rate of 10 accepted candidates per rotation period up
//! to the max membership threshold. Once this threshold is met, candidate selections //! to the max membership threshold. Once this threshold is met, candidate selections
//! are stalled until there is space for new members to join. This can be resolved by //! are stalled until there is space for new members to join. This can be resolved by
//! voting out existing members through the random challenges or by using governance //! voting out existing members through the random challenges or by using governance
//! to increase the maximum membership count. //! to increase the maximum membership count.
//! //!
//! ### User Life Cycle //! ### User Life Cycle
//! //!
//! A user can go through the following phases: //! A user can go through the following phases:
//! //!
//! ```ignore //! ```ignore
//! +-------> User <----------+ //! +-------> User <----------+
//! | + | //! | + |
...@@ -115,40 +115,40 @@ ...@@ -115,40 +115,40 @@
//! | | //! | |
//! +------------------Society---------------------+ //! +------------------Society---------------------+
//! ``` //! ```
//! //!
//! #### Initialization //! #### Initialization
//! //!
//! The society is initialized with a single member who is automatically chosen as the Head. //! The society is initialized with a single member who is automatically chosen as the Head.
//! //!
//! #### Bid Phase //! #### Bid Phase
//! //!
//! New users must have a bid to join the society. //! New users must have a bid to join the society.
//! //!
//! A user can make a bid by reserving a deposit. Alternatively, an already existing member //! A user can make a bid by reserving a deposit. Alternatively, an already existing member
//! can create a bid on a user's behalf by "vouching" for them. //! can create a bid on a user's behalf by "vouching" for them.
//! //!
//! A bid includes reward information that the user would like to receive for joining //! A bid includes reward information that the user would like to receive for joining
//! the society. A vouching bid can additionally request some portion of that reward as a tip //! the society. A vouching bid can additionally request some portion of that reward as a tip
//! to the voucher for vouching for the prospective candidate. //! to the voucher for vouching for the prospective candidate.
//! //!
//! Every rotation period, Bids are ordered by reward amount, and the module //! Every rotation period, Bids are ordered by reward amount, and the module
//! selects as many bids the Society Pot can support for that period. //! selects as many bids the Society Pot can support for that period.
//! //!
//! These selected bids become candidates and move on to the Candidate phase. //! These selected bids become candidates and move on to the Candidate phase.
//! Bids that were not selected stay in the bidder pool until they are selected or //! Bids that were not selected stay in the bidder pool until they are selected or
//! a user chooses to "unbid". //! a user chooses to "unbid".
//! //!
//! #### Candidate Phase //! #### Candidate Phase
//! //!
//! Once a bidder becomes a candidate, members vote whether to approve or reject //! Once a bidder becomes a candidate, members vote whether to approve or reject
//! that candidate into society. This voting process also happens during a rotation period. //! that candidate into society. This voting process also happens during a rotation period.
//! //!
//! The approval and rejection criteria for candidates are not set on chain, //! The approval and rejection criteria for candidates are not set on chain,
//! and may change for different societies. //! and may change for different societies.
//! //!
//! At the end of the rotation period, we collect the votes for a candidate //! At the end of the rotation period, we collect the votes for a candidate
//! and randomly select a vote as the final outcome. //! and randomly select a vote as the final outcome.
//! //!
//! ```ignore //! ```ignore
//! [ a-accept, r-reject, s-skeptic ] //! [ a-accept, r-reject, s-skeptic ]
//! +----------------------------------+ //! +----------------------------------+
...@@ -163,63 +163,63 @@ ...@@ -163,63 +163,63 @@
//! //!
//! Result: Rejected //! Result: Rejected
//! ``` //! ```
//! //!
//! Each member that voted opposite to this randomly selected vote is punished by //! Each member that voted opposite to this randomly selected vote is punished by
//! slashing their unclaimed payouts and increasing the number of strikes they have. //! slashing their unclaimed payouts and increasing the number of strikes they have.
//! //!
//! These slashed funds are given to a random user who voted the same as the //! These slashed funds are given to a random user who voted the same as the
//! selected vote as a reward for participating in the vote. //! selected vote as a reward for participating in the vote.
//! //!
//! If the candidate wins the vote, they receive their bid reward as a future payout. //! If the candidate wins the vote, they receive their bid reward as a future payout.
//! If the bid was placed by a voucher, they will receive their portion of the reward, //! If the bid was placed by a voucher, they will receive their portion of the reward,
//! before the rest is paid to the winning candidate. //! before the rest is paid to the winning candidate.
//! //!
//! One winning candidate is selected as the Head of the members. This is randomly //! One winning candidate is selected as the Head of the members. This is randomly
//! chosen, weighted by the number of approvals the winning candidates accumulated. //! chosen, weighted by the number of approvals the winning candidates accumulated.
//! //!
//! If the candidate loses the vote, they are suspended and it is up to the Suspension //! If the candidate loses the vote, they are suspended and it is up to the Suspension
//! Judgement origin to determine if the candidate should go through the bidding process //! Judgement origin to determine if the candidate should go through the bidding process
//! again, should be accepted into the membership society, or rejected and their deposit //! again, should be accepted into the membership society, or rejected and their deposit
//! slashed. //! slashed.
//! //!
//! #### Member Phase //! #### Member Phase
//! //!
//! Once a candidate becomes a member, their role is to participate in society. //! Once a candidate becomes a member, their role is to participate in society.
//! //!
//! Regular participation involves voting on candidates who want to join the membership //! Regular participation involves voting on candidates who want to join the membership
//! society, and by voting in the right way, a member will accumulate future payouts. //! society, and by voting in the right way, a member will accumulate future payouts.
//! When a payout matures, members are able to claim those payouts. //! When a payout matures, members are able to claim those payouts.
//! //!
//! Members can also vouch for users to join the society, and request a "tip" from //! Members can also vouch for users to join the society, and request a "tip" from
//! the fees the new member would collect by joining the society. This vouching //! the fees the new member would collect by joining the society. This vouching
//! process is useful in situations where a user may not have enough balance to //! process is useful in situations where a user may not have enough balance to
//! satisfy the bid deposit. A member can only vouch one user at a time. //! satisfy the bid deposit. A member can only vouch one user at a time.
//! //!
//! During rotation periods, a random group of members are selected as "skeptics". //! During rotation periods, a random group of members are selected as "skeptics".
//! These skeptics are expected to vote on the current candidates. If they do not vote, //! These skeptics are expected to vote on the current candidates. If they do not vote,
//! their skeptic status is treated as a rejection vote, the member is deemed //! their skeptic status is treated as a rejection vote, the member is deemed
//! "lazy", and are given a strike per missing vote. //! "lazy", and are given a strike per missing vote.
//! //!
//! There is a challenge period in parallel to the rotation period. During a challenge period, //! There is a challenge period in parallel to the rotation period. During a challenge period,
//! a random member is selected to defend their membership to the society. Other members //! a random member is selected to defend their membership to the society. Other members
//! make a traditional majority-wins vote to determine if the member should stay in the society. //! make a traditional majority-wins vote to determine if the member should stay in the society.
//! Ties are treated as a failure of the challenge. //! Ties are treated as a failure of the challenge.
//! //!
//! If a member accumulates too many strikes or fails their membership challenge, //! If a member accumulates too many strikes or fails their membership challenge,
//! they will become suspended. While a member is suspended, they are unable to //! they will become suspended. While a member is suspended, they are unable to
//! claim matured payouts. It is up to the Suspension Judgement origin to determine //! claim matured payouts. It is up to the Suspension Judgement origin to determine
//! if the member should re-enter society or be removed from society with all their //! if the member should re-enter society or be removed from society with all their
//! future payouts slashed. //! future payouts slashed.
//! //!
//! ## Interface //! ## Interface
//! //!
//! ### Dispatchable Functions //! ### Dispatchable Functions
//! //!
//! #### For General Users //! #### For General Users
//! //!
//! * `bid` - A user can make a bid to join the membership society by reserving a deposit. //! * `bid` - A user can make a bid to join the membership society by reserving a deposit.
//! * `unbid` - A user can withdraw their bid for entry, the deposit is returned. //! * `unbid` - A user can withdraw their bid for entry, the deposit is returned.
//! //!
//! #### For Members //! #### For Members
//! //!
//! * `vouch` - A member can place a bid on behalf of a user to join the membership society. //! * `vouch` - A member can place a bid on behalf of a user to join the membership society.
...@@ -228,9 +228,9 @@ ...@@ -228,9 +228,9 @@
//! * `defender_vote` - A member can vote to approve or reject a defender's continued membership //! * `defender_vote` - A member can vote to approve or reject a defender's continued membership
//! to the society. //! to the society.
//! * `payout` - A member can claim their first matured payment. //! * `payout` - A member can claim their first matured payment.
//! //!
//! #### For Super Users //! #### For Super Users
//! //!
//! * `found` - The founder origin can initiate this society. Useful for bootstrapping the Society //! * `found` - The founder origin can initiate this society. Useful for bootstrapping the Society
//! pallet on an already running chain. //! pallet on an already running chain.
//! * `judge_suspended_member` - The suspension judgement origin is able to make //! * `judge_suspended_member` - The suspension judgement origin is able to make
...@@ -305,7 +305,7 @@ pub trait Trait<I=DefaultInstance>: system::Trait { ...@@ -305,7 +305,7 @@ pub trait Trait<I=DefaultInstance>: system::Trait {
type MaxLockDuration: Get<Self::BlockNumber>; type MaxLockDuration: Get<Self::BlockNumber>;
/// The origin that is allowed to call `found`. /// The origin that is allowed to call `found`.
type FounderOrigin: EnsureOrigin<Self::Origin>; type FounderSetOrigin: EnsureOrigin<Self::Origin>;
/// The origin that is allowed to make suspension judgements. /// The origin that is allowed to make suspension judgements.
type SuspensionJudgementOrigin: EnsureOrigin<Self::Origin>; type SuspensionJudgementOrigin: EnsureOrigin<Self::Origin>;
...@@ -400,6 +400,10 @@ impl<AccountId: PartialEq, Balance> BidKind<AccountId, Balance> { ...@@ -400,6 +400,10 @@ impl<AccountId: PartialEq, Balance> BidKind<AccountId, Balance> {
// This module's storage items. // This module's storage items.
decl_storage! { decl_storage! {
trait Store for Module<T: Trait<I>, I: Instance=DefaultInstance> as Society { trait Store for Module<T: Trait<I>, I: Instance=DefaultInstance> as Society {
/// The first member.
pub Founder get(founder) build(|config: &GenesisConfig<T, I>| config.members.first().cloned()):
Option<T::AccountId>;
/// The current set of candidates; bidders that are attempting to become members. /// The current set of candidates; bidders that are attempting to become members.
pub Candidates get(candidates): Vec<Bid<T::AccountId, BalanceOf<T, I>>>; pub Candidates get(candidates): Vec<Bid<T::AccountId, BalanceOf<T, I>>>;
...@@ -444,7 +448,7 @@ decl_storage! { ...@@ -444,7 +448,7 @@ decl_storage! {
/// The defending member currently being challenged. /// The defending member currently being challenged.
Defender get(fn defender): Option<T::AccountId>; Defender get(fn defender): Option<T::AccountId>;
/// Votes for the defender. /// Votes for the defender.
DefenderVotes: map hasher(twox_64_concat) T::AccountId => Option<Vote>; DefenderVotes: map hasher(twox_64_concat) T::AccountId => Option<Vote>;
...@@ -796,26 +800,26 @@ decl_module! { ...@@ -796,26 +800,26 @@ decl_module! {
/// This is done as a discrete action in order to allow for the /// This is done as a discrete action in order to allow for the
/// module to be included into a running chain and can only be done once. /// module to be included into a running chain and can only be done once.
/// ///
/// The dispatch origin for this call must be from the _FounderOrigin_. /// The dispatch origin for this call must be from the _FounderSetOrigin_.
/// ///
/// Parameters: /// Parameters:
/// - `founder` - The first member and head of the newly founded society. /// - `founder` - The first member and head of the newly founded society.
/// ///
/// # <weight> /// # <weight>
/// - One storage read to check `Head`. O(1) /// - Two storage mutates to set `Head` and `Founder`. O(1)
/// - One storage write to add the first member to society. O(1) /// - One storage write to add the first member to society. O(1)
/// - One storage write to add new Head. O(1)
/// - One event. /// - One event.
/// ///
/// Total Complexity: O(1) /// Total Complexity: O(1)
/// # </weight> /// # </weight>
#[weight = SimpleDispatchInfo::FixedNormal(10_000)] #[weight = SimpleDispatchInfo::FixedNormal(10_000)]
fn found(origin, founder: T::AccountId) { fn found(origin, founder: T::AccountId) {
T::FounderOrigin::ensure_origin(origin)?; T::FounderSetOrigin::ensure_origin(origin)?;
ensure!(!<Head<T, I>>::exists(), Error::<T, I>::AlreadyFounded); ensure!(!<Head<T, I>>::exists(), Error::<T, I>::AlreadyFounded);
// This should never fail in the context of this function... // This should never fail in the context of this function...
Self::add_member(&founder)?; Self::add_member(&founder)?;
<Head<T, I>>::put(&founder); <Head<T, I>>::put(&founder);
<Founder<T, I>>::put(&founder);
Self::deposit_event(RawEvent::Founded(founder)); Self::deposit_event(RawEvent::Founded(founder));
} }
/// Allow suspension judgement origin to make judgement on a suspended member. /// Allow suspension judgement origin to make judgement on a suspended member.
...@@ -849,7 +853,7 @@ decl_module! { ...@@ -849,7 +853,7 @@ decl_module! {
fn judge_suspended_member(origin, who: T::AccountId, forgive: bool) { fn judge_suspended_member(origin, who: T::AccountId, forgive: bool) {
T::SuspensionJudgementOrigin::ensure_origin(origin)?; T::SuspensionJudgementOrigin::ensure_origin(origin)?;
ensure!(<SuspendedMembers<T, I>>::exists(&who), Error::<T, I>::NotSuspended); ensure!(<SuspendedMembers<T, I>>::exists(&who), Error::<T, I>::NotSuspended);
if forgive { if forgive {
// Try to add member back to society. Can fail with `MaxMembers` limit. // Try to add member back to society. Can fail with `MaxMembers` limit.
Self::add_member(&who)?; Self::add_member(&who)?;
...@@ -1010,33 +1014,35 @@ decl_error! { ...@@ -1010,33 +1014,35 @@ decl_error! {
pub enum Error for Module<T: Trait<I>, I: Instance> { pub enum Error for Module<T: Trait<I>, I: Instance> {
/// An incorrect position was provided. /// An incorrect position was provided.
BadPosition, BadPosition,
/// User is not a member /// User is not a member.
NotMember, NotMember,
/// User is already a member /// User is already a member.
AlreadyMember, AlreadyMember,
/// User is suspended /// User is suspended.
Suspended, Suspended,
/// User is not suspended /// User is not suspended.
NotSuspended, NotSuspended,
/// Nothing to payout /// Nothing to payout.
NoPayout, NoPayout,
/// Society already founded /// Society already founded.
AlreadyFounded, AlreadyFounded,
/// Not enough in pot to accept candidate /// Not enough in pot to accept candidate.
InsufficientPot, InsufficientPot,
/// Member is already vouching or banned from vouching again /// Member is already vouching or banned from vouching again.
AlreadyVouching, AlreadyVouching,
/// Member is not vouching /// Member is not vouching.
NotVouching, NotVouching,
/// Cannot remove head /// Cannot remove the head of the chain.
Head, Head,
/// User has already made a bid /// Cannot remove the founder.
Founder,
/// User has already made a bid.
AlreadyBid, AlreadyBid,
/// User is already a candidate /// User is already a candidate.
AlreadyCandidate, AlreadyCandidate,
/// User is not a candidate /// User is not a candidate.
NotCandidate, NotCandidate,
/// Too many members in the society /// Too many members in the society.
MaxMembers, MaxMembers,
} }
} }
...@@ -1081,6 +1087,18 @@ decl_event! { ...@@ -1081,6 +1087,18 @@ decl_event! {
} }
} }
/// Simple ensure origin struct to filter for the founder account.
pub struct EnsureFounder<T>(sp_std::marker::PhantomData<T>);
impl<T: Trait> EnsureOrigin<T::Origin> for EnsureFounder<T> {
type Success = T::AccountId;
fn try_origin(o: T::Origin) -> Result<Self::Success, T::Origin> {
o.into().and_then(|o| match (o, Founder::<T>::get()) {
(system::RawOrigin::Signed(ref who), Some(ref f)) if who == f => Ok(who.clone()),
(r, _) => Err(T::Origin::from(r)),
})
}
}
/// Pick an item at pseudo-random from the slice, given the `rng`. `None` iff the slice is empty. /// Pick an item at pseudo-random from the slice, given the `rng`. `None` iff the slice is empty.
fn pick_item<'a, R: RngCore, T>(rng: &mut R, items: &'a [T]) -> Option<&'a T> { fn pick_item<'a, R: RngCore, T>(rng: &mut R, items: &'a [T]) -> Option<&'a T> {
if items.is_empty() { if items.is_empty() {
...@@ -1201,6 +1219,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> { ...@@ -1201,6 +1219,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
/// removes them from the Members storage item. /// removes them from the Members storage item.
pub fn remove_member(m: &T::AccountId) -> DispatchResult { pub fn remove_member(m: &T::AccountId) -> DispatchResult {
ensure!(Self::head() != Some(m.clone()), Error::<T, I>::Head); ensure!(Self::head() != Some(m.clone()), Error::<T, I>::Head);
ensure!(Self::founder() != Some(m.clone()), Error::<T, I>::Founder);
<Members<T, I>>::mutate(|members| <Members<T, I>>::mutate(|members|
match members.binary_search(&m) { match members.binary_search(&m) {
...@@ -1251,7 +1270,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> { ...@@ -1251,7 +1270,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
.filter_map(|m| <Votes<T, I>>::take(&candidate, m).map(|v| (v, m))) .filter_map(|m| <Votes<T, I>>::take(&candidate, m).map(|v| (v, m)))
.inspect(|&(v, _)| if v == Vote::Approve { approval_count += 1 }) .inspect(|&(v, _)| if v == Vote::Approve { approval_count += 1 })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Select one of the votes at random. // Select one of the votes at random.
// Note that `Vote::Skeptical` and `Vote::Reject` both reject the candidate. // Note that `Vote::Skeptical` and `Vote::Reject` both reject the candidate.
let is_accepted = pick_item(&mut rng, &votes).map(|x| x.0) == Some(Vote::Approve); let is_accepted = pick_item(&mut rng, &votes).map(|x| x.0) == Some(Vote::Approve);
...@@ -1325,7 +1344,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> { ...@@ -1325,7 +1344,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
// if at least one candidate was accepted... // if at least one candidate was accepted...
if !accepted.is_empty() { if !accepted.is_empty() {
// select one as primary, randomly chosen from the accepted, weighted by approvals. // select one as primary, randomly chosen from the accepted, weighted by approvals.
// Choose a random number between 0 and `total_approvals` // Choose a random number between 0 and `total_approvals`
let primary_point = pick_usize(&mut rng, total_approvals - 1); let primary_point = pick_usize(&mut rng, total_approvals - 1);
// Find the zero bid or the user who falls on that point // Find the zero bid or the user who falls on that point
...@@ -1333,7 +1352,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> { ...@@ -1333,7 +1352,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
.expect("e.1 of final item == total_approvals; \ .expect("e.1 of final item == total_approvals; \
worst case find will always return that item; qed") worst case find will always return that item; qed")
.0.clone(); .0.clone();
let accounts = accepted.into_iter().map(|x| x.0).collect::<Vec<_>>(); let accounts = accepted.into_iter().map(|x| x.0).collect::<Vec<_>>();
// Then write everything back out, signal the changed membership and leave an event. // Then write everything back out, signal the changed membership and leave an event.
...@@ -1509,8 +1528,10 @@ impl<T: Trait<I>, I: Instance> Module<T, I> { ...@@ -1509,8 +1528,10 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
/// the number of bids would not surpass `MaxMembers` if all were accepted. /// the number of bids would not surpass `MaxMembers` if all were accepted.
/// ///
/// May be empty. /// May be empty.
pub fn take_selected(members_len: usize, pot: BalanceOf<T, I>) -> Vec<Bid<T::AccountId, BalanceOf<T, I>>> pub fn take_selected(
{ members_len: usize,
pot: BalanceOf<T, I>
) -> Vec<Bid<T::AccountId, BalanceOf<T, I>>> {
let max_members = MaxMembers::<I>::get() as usize; let max_members = MaxMembers::<I>::get() as usize;
// No more than 10 will be returned. // No more than 10 will be returned.
let mut max_selections: usize = 10.min(max_members.saturating_sub(members_len)); let mut max_selections: usize = 10.min(max_members.saturating_sub(members_len));
...@@ -1521,7 +1542,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> { ...@@ -1521,7 +1542,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
// The list of selected candidates // The list of selected candidates
let mut selected = Vec::new(); let mut selected = Vec::new();
if bids.len() > 0 { if bids.len() > 0 {
// Can only select at most the length of bids // Can only select at most the length of bids
max_selections = max_selections.min(bids.len()); max_selections = max_selections.min(bids.len());
......
...@@ -104,7 +104,7 @@ impl Trait for Test { ...@@ -104,7 +104,7 @@ impl Trait for Test {
type MembershipChanged = (); type MembershipChanged = ();
type RotationPeriod = RotationPeriod; type RotationPeriod = RotationPeriod;
type MaxLockDuration = MaxLockDuration; type MaxLockDuration = MaxLockDuration;
type FounderOrigin = EnsureSignedBy<FounderSetAccount, u128>; type FounderSetOrigin = EnsureSignedBy<FounderSetAccount, u128>;
type SuspensionJudgementOrigin = EnsureSignedBy<SuspensionJudgementSetAccount, u128>; type SuspensionJudgementOrigin = EnsureSignedBy<SuspensionJudgementSetAccount, u128>;
type ChallengePeriod = ChallengePeriod; type ChallengePeriod = ChallengePeriod;
} }
...@@ -133,6 +133,9 @@ impl EnvBuilder { ...@@ -133,6 +133,9 @@ impl EnvBuilder {
(40, 50), (40, 50),
(50, 50), (50, 50),
(60, 50), (60, 50),
(70, 50),
(80, 50),
(90, 50),
], ],
pot: 0, pot: 0,
max_members: 100, max_members: 100,
......
...@@ -25,6 +25,8 @@ use sp_runtime::traits::BadOrigin; ...@@ -25,6 +25,8 @@ use sp_runtime::traits::BadOrigin;
#[test] #[test]
fn founding_works() { fn founding_works() {
EnvBuilder::new().with_members(vec![]).execute(|| { EnvBuilder::new().with_members(vec![]).execute(|| {
// No founder initially.
assert_eq!(Society::founder(), None);
// Account 1 is set as the founder origin // Account 1 is set as the founder origin
// Account 5 cannot start a society // Account 5 cannot start a society
assert_noop!(Society::found(Origin::signed(5), 20), BadOrigin); assert_noop!(Society::found(Origin::signed(5), 20), BadOrigin);
...@@ -34,6 +36,8 @@ fn founding_works() { ...@@ -34,6 +36,8 @@ fn founding_works() {
assert_eq!(Society::members(), vec![10]); assert_eq!(Society::members(), vec![10]);
// 10 is the head of the society // 10 is the head of the society
assert_eq!(Society::head(), Some(10)); assert_eq!(Society::head(), Some(10));
// ...and also the founder
assert_eq!(Society::founder(), Some(10));
// Cannot start another society // Cannot start another society
assert_noop!(Society::found(Origin::signed(1), 20), Error::<Test, _>::AlreadyFounded); assert_noop!(Society::found(Origin::signed(1), 20), Error::<Test, _>::AlreadyFounded);
}); });
...@@ -264,7 +268,7 @@ fn suspended_member_lifecycle_works() { ...@@ -264,7 +268,7 @@ fn suspended_member_lifecycle_works() {
// Suspended members cannot get payout // Suspended members cannot get payout
Society::bump_payout(&20, 10, 100); Society::bump_payout(&20, 10, 100);
assert_noop!(Society::payout(Origin::signed(20)), Error::<Test, _>::NotMember); assert_noop!(Society::payout(Origin::signed(20)), Error::<Test, _>::NotMember);
// Normal people cannot make judgement // Normal people cannot make judgement
assert_noop!(Society::judge_suspended_member(Origin::signed(20), 20, true), BadOrigin); assert_noop!(Society::judge_suspended_member(Origin::signed(20), 20, true), BadOrigin);
...@@ -460,10 +464,11 @@ fn unbid_vouch_works() { ...@@ -460,10 +464,11 @@ fn unbid_vouch_works() {
} }
#[test] #[test]
fn head_cannot_be_removed() { fn founder_and_head_cannot_be_removed() {
EnvBuilder::new().execute(|| { EnvBuilder::new().execute(|| {
// 10 is the only member and head // 10 is the only member, founder, and head
assert_eq!(Society::members(), vec![10]); assert_eq!(Society::members(), vec![10]);
assert_eq!(Society::founder(), Some(10));
assert_eq!(Society::head(), Some(10)); assert_eq!(Society::head(), Some(10));
// 10 can still accumulate strikes // 10 can still accumulate strikes
assert_ok!(Society::bid(Origin::signed(20), 0)); assert_ok!(Society::bid(Origin::signed(20), 0));
...@@ -485,16 +490,37 @@ fn head_cannot_be_removed() { ...@@ -485,16 +490,37 @@ fn head_cannot_be_removed() {
run_to_block(32); run_to_block(32);
assert_eq!(Society::members(), vec![10, 50]); assert_eq!(Society::members(), vec![10, 50]);
assert_eq!(Society::head(), Some(50)); assert_eq!(Society::head(), Some(50));
// Founder is unchanged
assert_eq!(Society::founder(), Some(10));
// 10 can now be suspended for strikes // 50 can still accumulate strikes
assert_ok!(Society::bid(Origin::signed(60), 0)); assert_ok!(Society::bid(Origin::signed(60), 0));
run_to_block(36);
// The candidate is rejected, so voting approve will give a strike
assert_ok!(Society::vote(Origin::signed(10), 60, true));
run_to_block(40); run_to_block(40);
assert_eq!(Strikes::<Test>::get(10), 0); assert_eq!(Strikes::<Test>::get(50), 1);
assert_eq!(<SuspendedMembers<Test>>::get(10), Some(())); assert_ok!(Society::bid(Origin::signed(70), 0));
assert_eq!(Society::members(), vec![50]); run_to_block(48);
assert_eq!(Strikes::<Test>::get(50), 2);
// Replace the head
assert_ok!(Society::bid(Origin::signed(80), 0));
run_to_block(52);
assert_ok!(Society::vote(Origin::signed(10), 80, true));
assert_ok!(Society::vote(Origin::signed(50), 80, true));
assert_ok!(Society::defender_vote(Origin::signed(10), true)); // Keep defender around
run_to_block(56);
assert_eq!(Society::members(), vec![10, 50, 80]);
assert_eq!(Society::head(), Some(80));
assert_eq!(Society::founder(), Some(10));
// 50 can now be suspended for strikes
assert_ok!(Society::bid(Origin::signed(90), 0));
run_to_block(60);
// The candidate is rejected, so voting approve will give a strike
assert_ok!(Society::vote(Origin::signed(50), 90, true));
run_to_block(64);
assert_eq!(Strikes::<Test>::get(50), 0);
assert_eq!(<SuspendedMembers<Test>>::get(50), Some(()));
assert_eq!(Society::members(), vec![10, 80]);
}); });
} }
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment