mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 04:41:03 +00:00
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: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
@@ -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 @@
|
|||||||
//! * 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 @@
|
|||||||
//! 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 @@
|
|||||||
//! 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 @@
|
|||||||
//! | |
|
//! | |
|
||||||
//! +------------------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 @@
|
|||||||
//!
|
//!
|
||||||
//! 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 @@
|
|||||||
//! * `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 {
|
|||||||
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> {
|
|||||||
// 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! {
|
|||||||
|
|
||||||
/// 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! {
|
|||||||
/// 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! {
|
|||||||
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! {
|
|||||||
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! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
|||||||
/// 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> {
|
|||||||
.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> {
|
|||||||
|
|
||||||
// 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> {
|
|||||||
.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> {
|
|||||||
/// 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> {
|
|||||||
|
|
||||||
// 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 {
|
|||||||
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 {
|
|||||||
(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;
|
|||||||
#[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() {
|
|||||||
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() {
|
|||||||
// 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() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[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() {
|
|||||||
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]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user