mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-06 06:48:01 +00:00
35e8e137c5
* cleanup <weight></weight> from docs comments * Changes to address review commnets * Fix CI cargo test --docs --------- Co-authored-by: parity-processbot <>
1743 lines
61 KiB
Rust
1743 lines
61 KiB
Rust
// This file is part of Substrate.
|
|
|
|
// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! # Society Pallet
|
|
//!
|
|
//! - [`Config`]
|
|
//! - [`Call`]
|
|
//!
|
|
//! ## Overview
|
|
//!
|
|
//! The Society pallet is an economic game which incentivizes users to participate
|
|
//! and maintain a membership society.
|
|
//!
|
|
//! ### User Types
|
|
//!
|
|
//! At any point, a user in the society can be one of a:
|
|
//! * Bidder - A user who has submitted intention of joining the society.
|
|
//! * Candidate - A user who will be voted on to join the society.
|
|
//! * Suspended Candidate - A user who failed to win a vote.
|
|
//! * Member - A user who is a member of the society.
|
|
//! * Suspended Member - A member of the society who has accumulated too many strikes
|
|
//! or failed their membership challenge.
|
|
//!
|
|
//! Of the non-suspended members, there is always a:
|
|
//! * Head - A member who is exempt from suspension.
|
|
//! * 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
|
|
//! "skeptics". The mechanics of skeptics is explained in the
|
|
//! [member phase](#member-phase) below.
|
|
//!
|
|
//! ### Mechanics
|
|
//!
|
|
//! #### Rewards
|
|
//!
|
|
//! Members are incentivized to participate in the society through rewards paid
|
|
//! by the Society treasury. These payments have a maturity period that the user
|
|
//! must wait before they are able to access the funds.
|
|
//!
|
|
//! #### Punishments
|
|
//!
|
|
//! Members can be punished by slashing the reward payouts that have not been
|
|
//! collected. Additionally, members can accumulate "strikes", and when they
|
|
//! reach a max strike limit, they become suspended.
|
|
//!
|
|
//! #### 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,
|
|
//! their skeptic status is treated as a rejection vote, the member is deemed
|
|
//! "lazy", and are given a strike per missing vote.
|
|
//!
|
|
//! #### Membership Challenges
|
|
//!
|
|
//! Every challenge rotation period, an existing member will be randomly selected
|
|
//! to defend their membership into society. Then, other members can vote whether
|
|
//! this defender should stay in society. A simple majority wins vote will determine
|
|
//! the outcome of the user. Ties are treated as a failure of the challenge, but
|
|
//! 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
|
|
//! negative outcome of a membership challenge.
|
|
//!
|
|
//! #### Society Treasury
|
|
//!
|
|
//! The membership society is independently funded by a treasury managed by this
|
|
//! pallet. Some subset of this treasury is placed in a Society Pot, which is used
|
|
//! to determine the number of accepted bids.
|
|
//!
|
|
//! #### Rate of Growth
|
|
//!
|
|
//! 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
|
|
//! 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
|
|
//! to increase the maximum membership count.
|
|
//!
|
|
//! ### User Life Cycle
|
|
//!
|
|
//! A user can go through the following phases:
|
|
//!
|
|
//! ```ignore
|
|
//! +-------> User <----------+
|
|
//! | + |
|
|
//! | | |
|
|
//! +----------------------------------------------+
|
|
//! | | | | |
|
|
//! | | v | |
|
|
//! | | Bidder <-----------+ |
|
|
//! | | + | |
|
|
//! | | | + |
|
|
//! | | v Suspended |
|
|
//! | | Candidate +----> Candidate |
|
|
//! | | + + |
|
|
//! | | | | |
|
|
//! | + | | |
|
|
//! | Suspended +------>| | |
|
|
//! | Member | | |
|
|
//! | ^ | | |
|
|
//! | | v | |
|
|
//! | +-------+ Member <----------+ |
|
|
//! | |
|
|
//! | |
|
|
//! +------------------Society---------------------+
|
|
//! ```
|
|
//!
|
|
//! #### Initialization
|
|
//!
|
|
//! The society is initialized with a single member who is automatically chosen as the Head.
|
|
//!
|
|
//! #### Bid Phase
|
|
//!
|
|
//! 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
|
|
//! 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
|
|
//! 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.
|
|
//!
|
|
//! Every rotation period, Bids are ordered by reward amount, and the pallet
|
|
//! selects as many bids the Society Pot can support for that period.
|
|
//!
|
|
//! 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
|
|
//! a user chooses to "unbid".
|
|
//!
|
|
//! #### Candidate Phase
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! The approval and rejection criteria for candidates are not set on chain,
|
|
//! and may change for different societies.
|
|
//!
|
|
//! At the end of the rotation period, we collect the votes for a candidate
|
|
//! and randomly select a vote as the final outcome.
|
|
//!
|
|
//! ```ignore
|
|
//! [ a-accept, r-reject, s-skeptic ]
|
|
//! +----------------------------------+
|
|
//! | |
|
|
//! | Member |0|1|2|3|4|5|6|7|8|9| |
|
|
//! | ----------------------------- |
|
|
//! | Vote |a|a|a|r|s|r|a|a|s|a| |
|
|
//! | ----------------------------- |
|
|
//! | Selected | | | |x| | | | | | | |
|
|
//! | |
|
|
//! +----------------------------------+
|
|
//!
|
|
//! Result: Rejected
|
|
//! ```
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! 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,
|
|
//! before the rest is paid to the winning candidate.
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! 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
|
|
//! again, should be accepted into the membership society, or rejected and their deposit
|
|
//! slashed.
|
|
//!
|
|
//! #### Member Phase
|
|
//!
|
|
//! 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
|
|
//! 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.
|
|
//!
|
|
//! 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
|
|
//! 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.
|
|
//!
|
|
//! 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,
|
|
//! their skeptic status is treated as a rejection vote, the member is deemed
|
|
//! "lazy", and are given a strike per missing vote.
|
|
//!
|
|
//! 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
|
|
//! 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.
|
|
//!
|
|
//! 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
|
|
//! 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
|
|
//! future payouts slashed.
|
|
//!
|
|
//! ## Interface
|
|
//!
|
|
//! ### Dispatchable Functions
|
|
//!
|
|
//! #### For General Users
|
|
//!
|
|
//! * `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.
|
|
//!
|
|
//! #### For Members
|
|
//!
|
|
//! * `vouch` - A member can place a bid on behalf of a user to join the membership society.
|
|
//! * `unvouch` - A member can revoke their vouch for a user.
|
|
//! * `vote` - A member can vote to approve or reject a candidate's request to join the society.
|
|
//! * `defender_vote` - A member can vote to approve or reject a defender's continued membership
|
|
//! to the society.
|
|
//! * `payout` - A member can claim their first matured payment.
|
|
//! * `unfound` - Allow the founder to unfound the society when they are the only member.
|
|
//!
|
|
//! #### For Super Users
|
|
//!
|
|
//! * `found` - The founder origin can initiate this society. Useful for bootstrapping the Society
|
|
//! pallet on an already running chain.
|
|
//! * `judge_suspended_member` - The suspension judgement origin is able to make
|
|
//! judgement on a suspended member.
|
|
//! * `judge_suspended_candidate` - The suspension judgement origin is able to
|
|
//! make judgement on a suspended candidate.
|
|
//! * `set_max_membership` - The ROOT origin can update the maximum member count for the society.
|
|
//! The max membership count must be greater than 1.
|
|
|
|
// Ensure we're `no_std` when compiling for Wasm.
|
|
#![cfg_attr(not(feature = "std"), no_std)]
|
|
|
|
#[cfg(test)]
|
|
mod mock;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
use frame_support::{
|
|
pallet_prelude::*,
|
|
traits::{
|
|
BalanceStatus, ChangeMembers, Currency, EnsureOrigin, ExistenceRequirement::AllowDeath,
|
|
Imbalance, OnUnbalanced, Randomness, ReservableCurrency,
|
|
},
|
|
PalletId,
|
|
};
|
|
use frame_system::pallet_prelude::*;
|
|
use rand_chacha::{
|
|
rand_core::{RngCore, SeedableRng},
|
|
ChaChaRng,
|
|
};
|
|
use scale_info::TypeInfo;
|
|
use sp_runtime::{
|
|
traits::{
|
|
AccountIdConversion, CheckedSub, Hash, IntegerSquareRoot, Saturating, StaticLookup,
|
|
TrailingZeroInput, Zero,
|
|
},
|
|
Percent, RuntimeDebug,
|
|
};
|
|
use sp_std::prelude::*;
|
|
|
|
pub use pallet::*;
|
|
|
|
type BalanceOf<T, I> =
|
|
<<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
|
type NegativeImbalanceOf<T, I> = <<T as Config<I>>::Currency as Currency<
|
|
<T as frame_system::Config>::AccountId,
|
|
>>::NegativeImbalance;
|
|
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
|
|
|
|
/// A vote by a member on a candidate application.
|
|
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
|
pub enum Vote {
|
|
/// The member has been chosen to be skeptic and has not yet taken any action.
|
|
Skeptic,
|
|
/// The member has rejected the candidate's application.
|
|
Reject,
|
|
/// The member approves of the candidate's application.
|
|
Approve,
|
|
}
|
|
|
|
/// A judgement by the suspension judgement origin on a suspended candidate.
|
|
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
|
pub enum Judgement {
|
|
/// The suspension judgement origin takes no direct judgment
|
|
/// and places the candidate back into the bid pool.
|
|
Rebid,
|
|
/// The suspension judgement origin has rejected the candidate's application.
|
|
Reject,
|
|
/// The suspension judgement origin approves of the candidate's application.
|
|
Approve,
|
|
}
|
|
|
|
/// Details of a payout given as a per-block linear "trickle".
|
|
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, Default, TypeInfo)]
|
|
pub struct Payout<Balance, BlockNumber> {
|
|
/// Total value of the payout.
|
|
value: Balance,
|
|
/// Block number at which the payout begins.
|
|
begin: BlockNumber,
|
|
/// Total number of blocks over which the payout is spread.
|
|
duration: BlockNumber,
|
|
/// Total value paid out so far.
|
|
paid: Balance,
|
|
}
|
|
|
|
/// Status of a vouching member.
|
|
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
|
pub enum VouchingStatus {
|
|
/// Member is currently vouching for a user.
|
|
Vouching,
|
|
/// Member is banned from vouching for other members.
|
|
Banned,
|
|
}
|
|
|
|
/// Number of strikes that a member has against them.
|
|
pub type StrikeCount = u32;
|
|
|
|
/// A bid for entry into society.
|
|
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
|
pub struct Bid<AccountId, Balance> {
|
|
/// The bidder/candidate trying to enter society
|
|
who: AccountId,
|
|
/// The kind of bid placed for this bidder/candidate. See `BidKind`.
|
|
kind: BidKind<AccountId, Balance>,
|
|
/// The reward that the bidder has requested for successfully joining the society.
|
|
value: Balance,
|
|
}
|
|
|
|
/// A vote by a member on a candidate application.
|
|
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
|
pub enum BidKind<AccountId, Balance> {
|
|
/// The CandidateDeposit was paid for this bid.
|
|
Deposit(Balance),
|
|
/// A member vouched for this bid. The account should be reinstated into `Members` once the
|
|
/// bid is successful (or if it is rescinded prior to launch).
|
|
Vouch(AccountId, Balance),
|
|
}
|
|
|
|
impl<AccountId: PartialEq, Balance> BidKind<AccountId, Balance> {
|
|
fn check_voucher(&self, v: &AccountId) -> DispatchResult {
|
|
if let BidKind::Vouch(ref a, _) = self {
|
|
if a == v {
|
|
Ok(())
|
|
} else {
|
|
Err("incorrect identity".into())
|
|
}
|
|
} else {
|
|
Err("not vouched".into())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[frame_support::pallet]
|
|
pub mod pallet {
|
|
use super::*;
|
|
|
|
#[pallet::pallet]
|
|
#[pallet::generate_store(pub(super) trait Store)]
|
|
#[pallet::without_storage_info]
|
|
pub struct Pallet<T, I = ()>(_);
|
|
|
|
#[pallet::config]
|
|
pub trait Config<I: 'static = ()>: frame_system::Config {
|
|
/// The overarching event type.
|
|
type RuntimeEvent: From<Event<Self, I>>
|
|
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
|
|
|
/// The societies's pallet id
|
|
#[pallet::constant]
|
|
type PalletId: Get<PalletId>;
|
|
|
|
/// The currency type used for bidding.
|
|
type Currency: ReservableCurrency<Self::AccountId>;
|
|
|
|
/// Something that provides randomness in the runtime.
|
|
type Randomness: Randomness<Self::Hash, Self::BlockNumber>;
|
|
|
|
/// The minimum amount of a deposit required for a bid to be made.
|
|
#[pallet::constant]
|
|
type CandidateDeposit: Get<BalanceOf<Self, I>>;
|
|
|
|
/// The amount of the unpaid reward that gets deducted in the case that either a skeptic
|
|
/// doesn't vote or someone votes in the wrong way.
|
|
#[pallet::constant]
|
|
type WrongSideDeduction: Get<BalanceOf<Self, I>>;
|
|
|
|
/// The number of times a member may vote the wrong way (or not at all, when they are a
|
|
/// skeptic) before they become suspended.
|
|
#[pallet::constant]
|
|
type MaxStrikes: Get<u32>;
|
|
|
|
/// The amount of incentive paid within each period. Doesn't include VoterTip.
|
|
#[pallet::constant]
|
|
type PeriodSpend: Get<BalanceOf<Self, I>>;
|
|
|
|
/// The receiver of the signal for when the members have changed.
|
|
type MembershipChanged: ChangeMembers<Self::AccountId>;
|
|
|
|
/// The number of blocks between candidate/membership rotation periods.
|
|
#[pallet::constant]
|
|
type RotationPeriod: Get<Self::BlockNumber>;
|
|
|
|
/// The maximum duration of the payout lock.
|
|
#[pallet::constant]
|
|
type MaxLockDuration: Get<Self::BlockNumber>;
|
|
|
|
/// The origin that is allowed to call `found`.
|
|
type FounderSetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
|
|
|
/// The origin that is allowed to make suspension judgements.
|
|
type SuspensionJudgementOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
|
|
|
/// The number of blocks between membership challenges.
|
|
#[pallet::constant]
|
|
type ChallengePeriod: Get<Self::BlockNumber>;
|
|
|
|
/// The maximum number of candidates that we accept per round.
|
|
#[pallet::constant]
|
|
type MaxCandidateIntake: Get<u32>;
|
|
}
|
|
|
|
#[pallet::error]
|
|
pub enum Error<T, I = ()> {
|
|
/// An incorrect position was provided.
|
|
BadPosition,
|
|
/// User is not a member.
|
|
NotMember,
|
|
/// User is already a member.
|
|
AlreadyMember,
|
|
/// User is suspended.
|
|
Suspended,
|
|
/// User is not suspended.
|
|
NotSuspended,
|
|
/// Nothing to payout.
|
|
NoPayout,
|
|
/// Society already founded.
|
|
AlreadyFounded,
|
|
/// Not enough in pot to accept candidate.
|
|
InsufficientPot,
|
|
/// Member is already vouching or banned from vouching again.
|
|
AlreadyVouching,
|
|
/// Member is not vouching.
|
|
NotVouching,
|
|
/// Cannot remove the head of the chain.
|
|
Head,
|
|
/// Cannot remove the founder.
|
|
Founder,
|
|
/// User has already made a bid.
|
|
AlreadyBid,
|
|
/// User is already a candidate.
|
|
AlreadyCandidate,
|
|
/// User is not a candidate.
|
|
NotCandidate,
|
|
/// Too many members in the society.
|
|
MaxMembers,
|
|
/// The caller is not the founder.
|
|
NotFounder,
|
|
/// The caller is not the head.
|
|
NotHead,
|
|
}
|
|
|
|
#[pallet::event]
|
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config<I>, I: 'static = ()> {
|
|
/// The society is founded by the given identity.
|
|
Founded { founder: T::AccountId },
|
|
/// A membership bid just happened. The given account is the candidate's ID and their offer
|
|
/// is the second.
|
|
Bid { candidate_id: T::AccountId, offer: BalanceOf<T, I> },
|
|
/// A membership bid just happened by vouching. The given account is the candidate's ID and
|
|
/// their offer is the second. The vouching party is the third.
|
|
Vouch { candidate_id: T::AccountId, offer: BalanceOf<T, I>, vouching: T::AccountId },
|
|
/// A candidate was dropped (due to an excess of bids in the system).
|
|
AutoUnbid { candidate: T::AccountId },
|
|
/// A candidate was dropped (by their request).
|
|
Unbid { candidate: T::AccountId },
|
|
/// A candidate was dropped (by request of who vouched for them).
|
|
Unvouch { candidate: T::AccountId },
|
|
/// A group of candidates have been inducted. The batch's primary is the first value, the
|
|
/// batch in full is the second.
|
|
Inducted { primary: T::AccountId, candidates: Vec<T::AccountId> },
|
|
/// A suspended member has been judged.
|
|
SuspendedMemberJudgement { who: T::AccountId, judged: bool },
|
|
/// A candidate has been suspended
|
|
CandidateSuspended { candidate: T::AccountId },
|
|
/// A member has been suspended
|
|
MemberSuspended { member: T::AccountId },
|
|
/// A member has been challenged
|
|
Challenged { member: T::AccountId },
|
|
/// A vote has been placed
|
|
Vote { candidate: T::AccountId, voter: T::AccountId, vote: bool },
|
|
/// A vote has been placed for a defending member
|
|
DefenderVote { voter: T::AccountId, vote: bool },
|
|
/// A new \[max\] member count has been set
|
|
NewMaxMembers { max: u32 },
|
|
/// Society is unfounded.
|
|
Unfounded { founder: T::AccountId },
|
|
/// Some funds were deposited into the society account.
|
|
Deposit { value: BalanceOf<T, I> },
|
|
/// A group of members has been choosen as Skeptics
|
|
SkepticsChosen { skeptics: Vec<T::AccountId> },
|
|
}
|
|
|
|
/// The first member.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn founder)]
|
|
pub type Founder<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId>;
|
|
|
|
/// A hash of the rules of this society concerning membership. Can only be set once and
|
|
/// only by the founder.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn rules)]
|
|
pub type Rules<T: Config<I>, I: 'static = ()> = StorageValue<_, T::Hash>;
|
|
|
|
/// The current set of candidates; bidders that are attempting to become members.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn candidates)]
|
|
pub type Candidates<T: Config<I>, I: 'static = ()> =
|
|
StorageValue<_, Vec<Bid<T::AccountId, BalanceOf<T, I>>>, ValueQuery>;
|
|
|
|
/// The set of suspended candidates.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn suspended_candidate)]
|
|
pub type SuspendedCandidates<T: Config<I>, I: 'static = ()> = StorageMap<
|
|
_,
|
|
Twox64Concat,
|
|
T::AccountId,
|
|
(BalanceOf<T, I>, BidKind<T::AccountId, BalanceOf<T, I>>),
|
|
>;
|
|
|
|
/// Amount of our account balance that is specifically for the next round's bid(s).
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn pot)]
|
|
pub type Pot<T: Config<I>, I: 'static = ()> = StorageValue<_, BalanceOf<T, I>, ValueQuery>;
|
|
|
|
/// The most primary from the most recently approved members.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn head)]
|
|
pub type Head<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId>;
|
|
|
|
/// The current set of members, ordered.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn members)]
|
|
pub type Members<T: Config<I>, I: 'static = ()> =
|
|
StorageValue<_, Vec<T::AccountId>, ValueQuery>;
|
|
|
|
/// The set of suspended members.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn suspended_member)]
|
|
pub type SuspendedMembers<T: Config<I>, I: 'static = ()> =
|
|
StorageMap<_, Twox64Concat, T::AccountId, bool, ValueQuery>;
|
|
|
|
/// The current bids, stored ordered by the value of the bid.
|
|
#[pallet::storage]
|
|
pub(super) type Bids<T: Config<I>, I: 'static = ()> =
|
|
StorageValue<_, Vec<Bid<T::AccountId, BalanceOf<T, I>>>, ValueQuery>;
|
|
|
|
/// Members currently vouching or banned from vouching again
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn vouching)]
|
|
pub(super) type Vouching<T: Config<I>, I: 'static = ()> =
|
|
StorageMap<_, Twox64Concat, T::AccountId, VouchingStatus>;
|
|
|
|
/// Pending payouts; ordered by block number, with the amount that should be paid out.
|
|
#[pallet::storage]
|
|
pub(super) type Payouts<T: Config<I>, I: 'static = ()> = StorageMap<
|
|
_,
|
|
Twox64Concat,
|
|
T::AccountId,
|
|
Vec<(T::BlockNumber, BalanceOf<T, I>)>,
|
|
ValueQuery,
|
|
>;
|
|
|
|
/// The ongoing number of losing votes cast by the member.
|
|
#[pallet::storage]
|
|
pub(super) type Strikes<T: Config<I>, I: 'static = ()> =
|
|
StorageMap<_, Twox64Concat, T::AccountId, StrikeCount, ValueQuery>;
|
|
|
|
/// Double map from Candidate -> Voter -> (Maybe) Vote.
|
|
#[pallet::storage]
|
|
pub(super) type Votes<T: Config<I>, I: 'static = ()> =
|
|
StorageDoubleMap<_, Twox64Concat, T::AccountId, Twox64Concat, T::AccountId, Vote>;
|
|
|
|
/// The defending member currently being challenged.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn defender)]
|
|
pub(super) type Defender<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId>;
|
|
|
|
/// Votes for the defender.
|
|
#[pallet::storage]
|
|
pub(super) type DefenderVotes<T: Config<I>, I: 'static = ()> =
|
|
StorageMap<_, Twox64Concat, T::AccountId, Vote>;
|
|
|
|
/// The max number of members for the society at one time.
|
|
#[pallet::storage]
|
|
#[pallet::getter(fn max_members)]
|
|
pub(super) type MaxMembers<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
|
|
|
|
#[pallet::hooks]
|
|
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
|
|
fn on_initialize(n: T::BlockNumber) -> Weight {
|
|
let mut members = vec![];
|
|
|
|
let mut weight = Weight::zero();
|
|
let weights = T::BlockWeights::get();
|
|
|
|
// Run a candidate/membership rotation
|
|
if (n % T::RotationPeriod::get()).is_zero() {
|
|
members = <Members<T, I>>::get();
|
|
Self::rotate_period(&mut members);
|
|
|
|
weight += weights.max_block / 20;
|
|
}
|
|
|
|
// Run a challenge rotation
|
|
if (n % T::ChallengePeriod::get()).is_zero() {
|
|
// Only read members if not already read.
|
|
if members.is_empty() {
|
|
members = <Members<T, I>>::get();
|
|
}
|
|
Self::rotate_challenge(&mut members);
|
|
|
|
weight += weights.max_block / 20;
|
|
}
|
|
|
|
weight
|
|
}
|
|
}
|
|
|
|
#[pallet::genesis_config]
|
|
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
|
|
pub pot: BalanceOf<T, I>,
|
|
pub members: Vec<T::AccountId>,
|
|
pub max_members: u32,
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
|
|
fn default() -> Self {
|
|
Self {
|
|
pot: Default::default(),
|
|
members: Default::default(),
|
|
max_members: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pallet::genesis_build]
|
|
impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> {
|
|
fn build(&self) {
|
|
Pot::<T, I>::put(self.pot);
|
|
MaxMembers::<T, I>::put(self.max_members);
|
|
let first_member = self.members.first();
|
|
if let Some(member) = first_member {
|
|
Founder::<T, I>::put(member.clone());
|
|
Head::<T, I>::put(member.clone());
|
|
};
|
|
let mut m = self.members.clone();
|
|
m.sort();
|
|
Members::<T, I>::put(m);
|
|
}
|
|
}
|
|
|
|
#[pallet::call]
|
|
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
|
/// A user outside of the society can make a bid for entry.
|
|
///
|
|
/// Payment: `CandidateDeposit` will be reserved for making a bid. It is returned
|
|
/// when the bid becomes a member, or if the bid calls `unbid`.
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_.
|
|
///
|
|
/// Parameters:
|
|
/// - `value`: A one time payment the bid would like to receive when joining the society.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + B + C + logM + logB + X)
|
|
/// - B (len of bids)
|
|
/// - C (len of candidates)
|
|
/// - M (len of members)
|
|
/// - X (balance reserve)
|
|
#[pallet::call_index(0)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn bid(origin: OriginFor<T>, value: BalanceOf<T, I>) -> DispatchResult {
|
|
let who = ensure_signed(origin)?;
|
|
ensure!(!<SuspendedCandidates<T, I>>::contains_key(&who), Error::<T, I>::Suspended);
|
|
ensure!(!<SuspendedMembers<T, I>>::contains_key(&who), Error::<T, I>::Suspended);
|
|
let bids = <Bids<T, I>>::get();
|
|
ensure!(!Self::is_bid(&bids, &who), Error::<T, I>::AlreadyBid);
|
|
let candidates = <Candidates<T, I>>::get();
|
|
ensure!(!Self::is_candidate(&candidates, &who), Error::<T, I>::AlreadyCandidate);
|
|
let members = <Members<T, I>>::get();
|
|
ensure!(!Self::is_member(&members, &who), Error::<T, I>::AlreadyMember);
|
|
|
|
let deposit = T::CandidateDeposit::get();
|
|
T::Currency::reserve(&who, deposit)?;
|
|
|
|
Self::put_bid(bids, &who, value, BidKind::Deposit(deposit));
|
|
Self::deposit_event(Event::<T, I>::Bid { candidate_id: who, offer: value });
|
|
Ok(())
|
|
}
|
|
|
|
/// A bidder can remove their bid for entry into society.
|
|
/// By doing so, they will have their candidate deposit returned or
|
|
/// they will unvouch their voucher.
|
|
///
|
|
/// Payment: The bid deposit is unreserved if the user made a bid.
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_ and a bidder.
|
|
///
|
|
/// Parameters:
|
|
/// - `pos`: Position in the `Bids` vector of the bid who wants to unbid.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(B + X)
|
|
/// - B (len of bids)
|
|
/// - X (balance unreserve)
|
|
#[pallet::call_index(1)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn unbid(origin: OriginFor<T>, pos: u32) -> DispatchResult {
|
|
let who = ensure_signed(origin)?;
|
|
|
|
let pos = pos as usize;
|
|
<Bids<T, I>>::mutate(|b| {
|
|
if pos < b.len() && b[pos].who == who {
|
|
// Either unreserve the deposit or free up the vouching member.
|
|
// In neither case can we do much if the action isn't completable, but there's
|
|
// no reason that either should fail.
|
|
match b.remove(pos).kind {
|
|
BidKind::Deposit(deposit) => {
|
|
let err_amount = T::Currency::unreserve(&who, deposit);
|
|
debug_assert!(err_amount.is_zero());
|
|
},
|
|
BidKind::Vouch(voucher, _) => {
|
|
<Vouching<T, I>>::remove(&voucher);
|
|
},
|
|
}
|
|
Self::deposit_event(Event::<T, I>::Unbid { candidate: who });
|
|
Ok(())
|
|
} else {
|
|
Err(Error::<T, I>::BadPosition.into())
|
|
}
|
|
})
|
|
}
|
|
|
|
/// As a member, vouch for someone to join society by placing a bid on their behalf.
|
|
///
|
|
/// There is no deposit required to vouch for a new bid, but a member can only vouch for
|
|
/// one bid at a time. If the bid becomes a suspended candidate and ultimately rejected by
|
|
/// the suspension judgement origin, the member will be banned from vouching again.
|
|
///
|
|
/// As a vouching member, you can claim a tip if the candidate is accepted. This tip will
|
|
/// be paid as a portion of the reward the member will receive for joining the society.
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_ and a member.
|
|
///
|
|
/// Parameters:
|
|
/// - `who`: The user who you would like to vouch for.
|
|
/// - `value`: The total reward to be paid between you and the candidate if they become
|
|
/// a member in the society.
|
|
/// - `tip`: Your cut of the total `value` payout when the candidate is inducted into
|
|
/// the society. Tips larger than `value` will be saturated upon payout.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + B + C + logM + logB + X)
|
|
/// - B (len of bids)
|
|
/// - C (len of candidates)
|
|
/// - M (len of members)
|
|
/// - X (balance reserve)
|
|
#[pallet::call_index(2)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn vouch(
|
|
origin: OriginFor<T>,
|
|
who: AccountIdLookupOf<T>,
|
|
value: BalanceOf<T, I>,
|
|
tip: BalanceOf<T, I>,
|
|
) -> DispatchResult {
|
|
let voucher = ensure_signed(origin)?;
|
|
let who = T::Lookup::lookup(who)?;
|
|
// Check user is not suspended.
|
|
ensure!(!<SuspendedCandidates<T, I>>::contains_key(&who), Error::<T, I>::Suspended);
|
|
ensure!(!<SuspendedMembers<T, I>>::contains_key(&who), Error::<T, I>::Suspended);
|
|
// Check user is not a bid or candidate.
|
|
let bids = <Bids<T, I>>::get();
|
|
ensure!(!Self::is_bid(&bids, &who), Error::<T, I>::AlreadyBid);
|
|
let candidates = <Candidates<T, I>>::get();
|
|
ensure!(!Self::is_candidate(&candidates, &who), Error::<T, I>::AlreadyCandidate);
|
|
// Check user is not already a member.
|
|
let members = <Members<T, I>>::get();
|
|
ensure!(!Self::is_member(&members, &who), Error::<T, I>::AlreadyMember);
|
|
// Check sender can vouch.
|
|
ensure!(Self::is_member(&members, &voucher), Error::<T, I>::NotMember);
|
|
ensure!(!<Vouching<T, I>>::contains_key(&voucher), Error::<T, I>::AlreadyVouching);
|
|
|
|
<Vouching<T, I>>::insert(&voucher, VouchingStatus::Vouching);
|
|
Self::put_bid(bids, &who, value, BidKind::Vouch(voucher.clone(), tip));
|
|
Self::deposit_event(Event::<T, I>::Vouch {
|
|
candidate_id: who,
|
|
offer: value,
|
|
vouching: voucher,
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
/// As a vouching member, unvouch a bid. This only works while vouched user is
|
|
/// only a bidder (and not a candidate).
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_ and a vouching member.
|
|
///
|
|
/// Parameters:
|
|
/// - `pos`: Position in the `Bids` vector of the bid who should be unvouched.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(B)
|
|
/// - B (len of bids)
|
|
#[pallet::call_index(3)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn unvouch(origin: OriginFor<T>, pos: u32) -> DispatchResult {
|
|
let voucher = ensure_signed(origin)?;
|
|
ensure!(
|
|
Self::vouching(&voucher) == Some(VouchingStatus::Vouching),
|
|
Error::<T, I>::NotVouching
|
|
);
|
|
|
|
let pos = pos as usize;
|
|
<Bids<T, I>>::mutate(|b| {
|
|
if pos < b.len() {
|
|
b[pos].kind.check_voucher(&voucher)?;
|
|
<Vouching<T, I>>::remove(&voucher);
|
|
let who = b.remove(pos).who;
|
|
Self::deposit_event(Event::<T, I>::Unvouch { candidate: who });
|
|
Ok(())
|
|
} else {
|
|
Err(Error::<T, I>::BadPosition.into())
|
|
}
|
|
})
|
|
}
|
|
|
|
/// As a member, vote on a candidate.
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_ and a member.
|
|
///
|
|
/// Parameters:
|
|
/// - `candidate`: The candidate that the member would like to bid on.
|
|
/// - `approve`: A boolean which says if the candidate should be approved (`true`) or
|
|
/// rejected (`false`).
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + logM + C)
|
|
/// - C (len of candidates)
|
|
/// - M (len of members)
|
|
#[pallet::call_index(4)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn vote(
|
|
origin: OriginFor<T>,
|
|
candidate: AccountIdLookupOf<T>,
|
|
approve: bool,
|
|
) -> DispatchResult {
|
|
let voter = ensure_signed(origin)?;
|
|
let candidate = T::Lookup::lookup(candidate)?;
|
|
let candidates = <Candidates<T, I>>::get();
|
|
ensure!(Self::is_candidate(&candidates, &candidate), Error::<T, I>::NotCandidate);
|
|
let members = <Members<T, I>>::get();
|
|
ensure!(Self::is_member(&members, &voter), Error::<T, I>::NotMember);
|
|
|
|
let vote = if approve { Vote::Approve } else { Vote::Reject };
|
|
<Votes<T, I>>::insert(&candidate, &voter, vote);
|
|
|
|
Self::deposit_event(Event::<T, I>::Vote { candidate, voter, vote: approve });
|
|
Ok(())
|
|
}
|
|
|
|
/// As a member, vote on the defender.
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_ and a member.
|
|
///
|
|
/// Parameters:
|
|
/// - `approve`: A boolean which says if the candidate should be
|
|
/// approved (`true`) or rejected (`false`).
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + logM)
|
|
/// - M (len of members)
|
|
#[pallet::call_index(5)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn defender_vote(origin: OriginFor<T>, approve: bool) -> DispatchResult {
|
|
let voter = ensure_signed(origin)?;
|
|
let members = <Members<T, I>>::get();
|
|
ensure!(Self::is_member(&members, &voter), Error::<T, I>::NotMember);
|
|
|
|
let vote = if approve { Vote::Approve } else { Vote::Reject };
|
|
<DefenderVotes<T, I>>::insert(&voter, vote);
|
|
|
|
Self::deposit_event(Event::<T, I>::DefenderVote { voter, vote: approve });
|
|
Ok(())
|
|
}
|
|
|
|
/// Transfer the first matured payout for the sender and remove it from the records.
|
|
///
|
|
/// NOTE: This extrinsic needs to be called multiple times to claim multiple matured
|
|
/// payouts.
|
|
///
|
|
/// Payment: The member will receive a payment equal to their first matured
|
|
/// payout to their free balance.
|
|
///
|
|
/// The dispatch origin for this call must be _Signed_ and a member with
|
|
/// payouts remaining.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + logM + P + X)
|
|
/// - M (len of members)
|
|
/// - P (number of payouts for a particular member)
|
|
/// - X (currency transfer call)
|
|
#[pallet::call_index(6)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn payout(origin: OriginFor<T>) -> DispatchResult {
|
|
let who = ensure_signed(origin)?;
|
|
|
|
let members = <Members<T, I>>::get();
|
|
ensure!(Self::is_member(&members, &who), Error::<T, I>::NotMember);
|
|
|
|
let mut payouts = <Payouts<T, I>>::get(&who);
|
|
if let Some((when, amount)) = payouts.first() {
|
|
if when <= &<frame_system::Pallet<T>>::block_number() {
|
|
T::Currency::transfer(&Self::payouts(), &who, *amount, AllowDeath)?;
|
|
payouts.remove(0);
|
|
if payouts.is_empty() {
|
|
<Payouts<T, I>>::remove(&who);
|
|
} else {
|
|
<Payouts<T, I>>::insert(&who, payouts);
|
|
}
|
|
return Ok(())
|
|
}
|
|
}
|
|
Err(Error::<T, I>::NoPayout.into())
|
|
}
|
|
|
|
/// Found the society.
|
|
///
|
|
/// This is done as a discrete action in order to allow for the
|
|
/// pallet to be included into a running chain and can only be done once.
|
|
///
|
|
/// The dispatch origin for this call must be from the _FounderSetOrigin_.
|
|
///
|
|
/// Parameters:
|
|
/// - `founder` - The first member and head of the newly founded society.
|
|
/// - `max_members` - The initial max number of members for the society.
|
|
/// - `rules` - The rules of this society concerning membership.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(1)
|
|
#[pallet::call_index(7)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn found(
|
|
origin: OriginFor<T>,
|
|
founder: AccountIdLookupOf<T>,
|
|
max_members: u32,
|
|
rules: Vec<u8>,
|
|
) -> DispatchResult {
|
|
T::FounderSetOrigin::ensure_origin(origin)?;
|
|
let founder = T::Lookup::lookup(founder)?;
|
|
ensure!(!<Head<T, I>>::exists(), Error::<T, I>::AlreadyFounded);
|
|
ensure!(max_members > 1, Error::<T, I>::MaxMembers);
|
|
// This should never fail in the context of this function...
|
|
<MaxMembers<T, I>>::put(max_members);
|
|
Self::add_member(&founder)?;
|
|
<Head<T, I>>::put(&founder);
|
|
<Founder<T, I>>::put(&founder);
|
|
Rules::<T, I>::put(T::Hashing::hash(&rules));
|
|
Self::deposit_event(Event::<T, I>::Founded { founder });
|
|
Ok(())
|
|
}
|
|
|
|
/// Annul the founding of the society.
|
|
///
|
|
/// The dispatch origin for this call must be Signed, and the signing account must be both
|
|
/// the `Founder` and the `Head`. This implies that it may only be done when there is one
|
|
/// member.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(1)
|
|
#[pallet::call_index(8)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn unfound(origin: OriginFor<T>) -> DispatchResult {
|
|
let founder = ensure_signed(origin)?;
|
|
ensure!(Founder::<T, I>::get() == Some(founder.clone()), Error::<T, I>::NotFounder);
|
|
ensure!(Head::<T, I>::get() == Some(founder.clone()), Error::<T, I>::NotHead);
|
|
|
|
Members::<T, I>::kill();
|
|
Head::<T, I>::kill();
|
|
Founder::<T, I>::kill();
|
|
Rules::<T, I>::kill();
|
|
Candidates::<T, I>::kill();
|
|
#[allow(deprecated)]
|
|
SuspendedCandidates::<T, I>::remove_all(None);
|
|
Self::deposit_event(Event::<T, I>::Unfounded { founder });
|
|
Ok(())
|
|
}
|
|
|
|
/// Allow suspension judgement origin to make judgement on a suspended member.
|
|
///
|
|
/// If a suspended member is forgiven, we simply add them back as a member, not affecting
|
|
/// any of the existing storage items for that member.
|
|
///
|
|
/// If a suspended member is rejected, remove all associated storage items, including
|
|
/// their payouts, and remove any vouched bids they currently have.
|
|
///
|
|
/// The dispatch origin for this call must be from the _SuspensionJudgementOrigin_.
|
|
///
|
|
/// Parameters:
|
|
/// - `who` - The suspended member to be judged.
|
|
/// - `forgive` - A boolean representing whether the suspension judgement origin forgives
|
|
/// (`true`) or rejects (`false`) a suspended member.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + logM + B)
|
|
/// - B (len of bids)
|
|
/// - M (len of members)
|
|
#[pallet::call_index(9)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn judge_suspended_member(
|
|
origin: OriginFor<T>,
|
|
who: AccountIdLookupOf<T>,
|
|
forgive: bool,
|
|
) -> DispatchResult {
|
|
T::SuspensionJudgementOrigin::ensure_origin(origin)?;
|
|
let who = T::Lookup::lookup(who)?;
|
|
ensure!(<SuspendedMembers<T, I>>::contains_key(&who), Error::<T, I>::NotSuspended);
|
|
|
|
if forgive {
|
|
// Try to add member back to society. Can fail with `MaxMembers` limit.
|
|
Self::add_member(&who)?;
|
|
} else {
|
|
// Cancel a suspended member's membership, remove their payouts.
|
|
<Payouts<T, I>>::remove(&who);
|
|
<Strikes<T, I>>::remove(&who);
|
|
// Remove their vouching status, potentially unbanning them in the future.
|
|
if <Vouching<T, I>>::take(&who) == Some(VouchingStatus::Vouching) {
|
|
// Try to remove their bid if they are vouching.
|
|
// If their vouch is already a candidate, do nothing.
|
|
<Bids<T, I>>::mutate(|bids|
|
|
// Try to find the matching bid
|
|
if let Some(pos) = bids.iter().position(|b| b.kind.check_voucher(&who).is_ok()) {
|
|
// Remove the bid, and emit an event
|
|
let vouched = bids.remove(pos).who;
|
|
Self::deposit_event(Event::<T, I>::Unvouch { candidate: vouched });
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
<SuspendedMembers<T, I>>::remove(&who);
|
|
Self::deposit_event(Event::<T, I>::SuspendedMemberJudgement { who, judged: forgive });
|
|
Ok(())
|
|
}
|
|
|
|
/// Allow suspended judgement origin to make judgement on a suspended candidate.
|
|
///
|
|
/// If the judgement is `Approve`, we add them to society as a member with the appropriate
|
|
/// payment for joining society.
|
|
///
|
|
/// If the judgement is `Reject`, we either slash the deposit of the bid, giving it back
|
|
/// to the society treasury, or we ban the voucher from vouching again.
|
|
///
|
|
/// If the judgement is `Rebid`, we put the candidate back in the bid pool and let them go
|
|
/// through the induction process again.
|
|
///
|
|
/// The dispatch origin for this call must be from the _SuspensionJudgementOrigin_.
|
|
///
|
|
/// Parameters:
|
|
/// - `who` - The suspended candidate to be judged.
|
|
/// - `judgement` - `Approve`, `Reject`, or `Rebid`.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(M + logM + B + X)
|
|
/// - B (len of bids)
|
|
/// - M (len of members)
|
|
/// - X (balance action)
|
|
#[pallet::call_index(10)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn judge_suspended_candidate(
|
|
origin: OriginFor<T>,
|
|
who: AccountIdLookupOf<T>,
|
|
judgement: Judgement,
|
|
) -> DispatchResult {
|
|
T::SuspensionJudgementOrigin::ensure_origin(origin)?;
|
|
let who = T::Lookup::lookup(who)?;
|
|
if let Some((value, kind)) = <SuspendedCandidates<T, I>>::get(&who) {
|
|
match judgement {
|
|
Judgement::Approve => {
|
|
// Suspension Judgement origin has approved this candidate
|
|
// Make sure we can pay them
|
|
let pot = Self::pot();
|
|
ensure!(pot >= value, Error::<T, I>::InsufficientPot);
|
|
// Try to add user as a member! Can fail with `MaxMember` limit.
|
|
Self::add_member(&who)?;
|
|
// Reduce next pot by payout
|
|
<Pot<T, I>>::put(pot - value);
|
|
// Add payout for new candidate
|
|
let maturity = <frame_system::Pallet<T>>::block_number() +
|
|
Self::lock_duration(Self::members().len() as u32);
|
|
Self::pay_accepted_candidate(&who, value, kind, maturity);
|
|
},
|
|
Judgement::Reject => {
|
|
// Founder has rejected this candidate
|
|
match kind {
|
|
BidKind::Deposit(deposit) => {
|
|
// Slash deposit and move it to the society account
|
|
let res = T::Currency::repatriate_reserved(
|
|
&who,
|
|
&Self::account_id(),
|
|
deposit,
|
|
BalanceStatus::Free,
|
|
);
|
|
debug_assert!(res.is_ok());
|
|
},
|
|
BidKind::Vouch(voucher, _) => {
|
|
// Ban the voucher from vouching again
|
|
<Vouching<T, I>>::insert(&voucher, VouchingStatus::Banned);
|
|
},
|
|
}
|
|
},
|
|
Judgement::Rebid => {
|
|
// Founder has taken no judgement, and candidate is placed back into the
|
|
// pool.
|
|
let bids = <Bids<T, I>>::get();
|
|
Self::put_bid(bids, &who, value, kind);
|
|
},
|
|
}
|
|
|
|
// Remove suspended candidate
|
|
<SuspendedCandidates<T, I>>::remove(who);
|
|
} else {
|
|
return Err(Error::<T, I>::NotSuspended.into())
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Allows root origin to change the maximum number of members in society.
|
|
/// Max membership count must be greater than 1.
|
|
///
|
|
/// The dispatch origin for this call must be from _ROOT_.
|
|
///
|
|
/// Parameters:
|
|
/// - `max` - The maximum number of members for the society.
|
|
///
|
|
/// ## Complexity
|
|
/// - O(1)
|
|
#[pallet::call_index(11)]
|
|
#[pallet::weight(T::BlockWeights::get().max_block / 10)]
|
|
pub fn set_max_members(origin: OriginFor<T>, max: u32) -> DispatchResult {
|
|
ensure_root(origin)?;
|
|
ensure!(max > 1, Error::<T, I>::MaxMembers);
|
|
MaxMembers::<T, I>::put(max);
|
|
Self::deposit_event(Event::<T, I>::NewMaxMembers { max });
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Simple ensure origin struct to filter for the founder account.
|
|
pub struct EnsureFounder<T>(sp_std::marker::PhantomData<T>);
|
|
impl<T: Config> EnsureOrigin<T::RuntimeOrigin> for EnsureFounder<T> {
|
|
type Success = T::AccountId;
|
|
fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
|
|
o.into().and_then(|o| match (o, Founder::<T>::get()) {
|
|
(frame_system::RawOrigin::Signed(ref who), Some(ref f)) if who == f => Ok(who.clone()),
|
|
(r, _) => Err(T::RuntimeOrigin::from(r)),
|
|
})
|
|
}
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
|
|
let founder = Founder::<T>::get().ok_or(())?;
|
|
Ok(T::RuntimeOrigin::from(frame_system::RawOrigin::Signed(founder)))
|
|
}
|
|
}
|
|
|
|
/// 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> {
|
|
if items.is_empty() {
|
|
None
|
|
} else {
|
|
Some(&items[pick_usize(rng, items.len() - 1)])
|
|
}
|
|
}
|
|
|
|
/// Pick a new PRN, in the range [0, `max`] (inclusive).
|
|
fn pick_usize<R: RngCore>(rng: &mut R, max: usize) -> usize {
|
|
(rng.next_u32() % (max as u32 + 1)) as usize
|
|
}
|
|
|
|
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
|
/// Puts a bid into storage ordered by smallest to largest value.
|
|
/// Allows a maximum of 1000 bids in queue, removing largest value people first.
|
|
fn put_bid(
|
|
mut bids: Vec<Bid<T::AccountId, BalanceOf<T, I>>>,
|
|
who: &T::AccountId,
|
|
value: BalanceOf<T, I>,
|
|
bid_kind: BidKind<T::AccountId, BalanceOf<T, I>>,
|
|
) {
|
|
const MAX_BID_COUNT: usize = 1000;
|
|
|
|
match bids.binary_search_by(|bid| bid.value.cmp(&value)) {
|
|
// Insert new elements after the existing ones. This ensures new bids
|
|
// with the same bid value are further down the list than existing ones.
|
|
Ok(pos) => {
|
|
let different_bid = bids
|
|
.iter()
|
|
// Easily extract the index we are on
|
|
.enumerate()
|
|
// Skip ahead to the suggested position
|
|
.skip(pos)
|
|
// Keep skipping ahead until the position changes
|
|
// Get the element when things changed
|
|
.find(|(_, x)| x.value > bids[pos].value);
|
|
|
|
// If the element is not at the end of the list, insert the new element
|
|
// in the spot.
|
|
if let Some((p, _)) = different_bid {
|
|
bids.insert(p, Bid { value, who: who.clone(), kind: bid_kind });
|
|
// If the element is at the end of the list, push the element on the end.
|
|
} else {
|
|
bids.push(Bid { value, who: who.clone(), kind: bid_kind });
|
|
}
|
|
},
|
|
Err(pos) => bids.insert(pos, Bid { value, who: who.clone(), kind: bid_kind }),
|
|
}
|
|
// Keep it reasonably small.
|
|
if bids.len() > MAX_BID_COUNT {
|
|
let Bid { who: popped, kind, .. } = bids.pop().expect("b.len() > 1000; qed");
|
|
match kind {
|
|
BidKind::Deposit(deposit) => {
|
|
let err_amount = T::Currency::unreserve(&popped, deposit);
|
|
debug_assert!(err_amount.is_zero());
|
|
},
|
|
BidKind::Vouch(voucher, _) => {
|
|
<Vouching<T, I>>::remove(&voucher);
|
|
},
|
|
}
|
|
Self::deposit_event(Event::<T, I>::AutoUnbid { candidate: popped });
|
|
}
|
|
|
|
<Bids<T, I>>::put(bids);
|
|
}
|
|
|
|
/// Check a user is a bid.
|
|
fn is_bid(bids: &Vec<Bid<T::AccountId, BalanceOf<T, I>>>, who: &T::AccountId) -> bool {
|
|
// Bids are ordered by `value`, so we cannot binary search for a user.
|
|
bids.iter().any(|bid| bid.who == *who)
|
|
}
|
|
|
|
/// Check a user is a candidate.
|
|
fn is_candidate(
|
|
candidates: &Vec<Bid<T::AccountId, BalanceOf<T, I>>>,
|
|
who: &T::AccountId,
|
|
) -> bool {
|
|
// Looking up a candidate is the same as looking up a bid
|
|
Self::is_bid(candidates, who)
|
|
}
|
|
|
|
/// Check a user is a member.
|
|
fn is_member(members: &Vec<T::AccountId>, who: &T::AccountId) -> bool {
|
|
members.binary_search(who).is_ok()
|
|
}
|
|
|
|
/// Add a member to the sorted members list. If the user is already a member, do nothing.
|
|
/// Can fail when `MaxMember` limit is reached, but has no side-effects.
|
|
fn add_member(who: &T::AccountId) -> DispatchResult {
|
|
let mut members = <Members<T, I>>::get();
|
|
ensure!(members.len() < MaxMembers::<T, I>::get() as usize, Error::<T, I>::MaxMembers);
|
|
match members.binary_search(who) {
|
|
// Add the new member
|
|
Err(i) => {
|
|
members.insert(i, who.clone());
|
|
T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members);
|
|
<Members<T, I>>::put(members);
|
|
Ok(())
|
|
},
|
|
// User is already a member, do nothing.
|
|
Ok(_) => Ok(()),
|
|
}
|
|
}
|
|
|
|
/// Remove a member from the members list, except the Head.
|
|
///
|
|
/// NOTE: This does not correctly clean up a member from storage. It simply
|
|
/// removes them from the Members storage item.
|
|
pub fn remove_member(m: &T::AccountId) -> DispatchResult {
|
|
ensure!(Self::head() != Some(m.clone()), Error::<T, I>::Head);
|
|
ensure!(Self::founder() != Some(m.clone()), Error::<T, I>::Founder);
|
|
|
|
let mut members = <Members<T, I>>::get();
|
|
match members.binary_search(m) {
|
|
Err(_) => Err(Error::<T, I>::NotMember.into()),
|
|
Ok(i) => {
|
|
members.remove(i);
|
|
T::MembershipChanged::change_members_sorted(&[], &[m.clone()], &members[..]);
|
|
<Members<T, I>>::put(members);
|
|
Ok(())
|
|
},
|
|
}
|
|
}
|
|
|
|
/// End the current period and begin a new one.
|
|
fn rotate_period(members: &mut Vec<T::AccountId>) {
|
|
let phrase = b"society_rotation";
|
|
|
|
let mut pot = <Pot<T, I>>::get();
|
|
|
|
// we'll need a random seed here.
|
|
// TODO: deal with randomness freshness
|
|
// https://github.com/paritytech/substrate/issues/8312
|
|
let (seed, _) = T::Randomness::random(phrase);
|
|
// seed needs to be guaranteed to be 32 bytes.
|
|
let seed = <[u8; 32]>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
|
|
.expect("input is padded with zeroes; qed");
|
|
let mut rng = ChaChaRng::from_seed(seed);
|
|
|
|
// we assume there's at least one member or this logic won't work.
|
|
if !members.is_empty() {
|
|
let candidates = <Candidates<T, I>>::take();
|
|
// NOTE: This may cause member length to surpass `MaxMembers`, but results in no
|
|
// consensus critical issues or side-effects. This is auto-correcting as members fall
|
|
// out of society.
|
|
members.reserve(candidates.len());
|
|
|
|
let maturity = <frame_system::Pallet<T>>::block_number() +
|
|
Self::lock_duration(members.len() as u32);
|
|
|
|
let mut rewardees = Vec::new();
|
|
let mut total_approvals = 0;
|
|
let mut total_slash = <BalanceOf<T, I>>::zero();
|
|
let mut total_payouts = <BalanceOf<T, I>>::zero();
|
|
|
|
let accepted = candidates
|
|
.into_iter()
|
|
.filter_map(|Bid { value, who: candidate, kind }| {
|
|
let mut approval_count = 0;
|
|
|
|
// Creates a vector of (vote, member) for the given candidate
|
|
// and tallies total number of approve votes for that candidate.
|
|
let votes = members
|
|
.iter()
|
|
.filter_map(|m| <Votes<T, I>>::take(&candidate, m).map(|v| (v, m)))
|
|
.inspect(|&(v, _)| {
|
|
if v == Vote::Approve {
|
|
approval_count += 1
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// Select one of the votes at random.
|
|
// 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 matching_vote = if is_accepted { Vote::Approve } else { Vote::Reject };
|
|
|
|
let bad_vote = |m: &T::AccountId| {
|
|
// Voter voted wrong way (or was just a lazy skeptic) then reduce their
|
|
// payout and increase their strikes. after MaxStrikes then they go into
|
|
// suspension.
|
|
let amount = Self::slash_payout(m, T::WrongSideDeduction::get());
|
|
|
|
let strikes = <Strikes<T, I>>::mutate(m, |s| {
|
|
*s += 1;
|
|
*s
|
|
});
|
|
if strikes >= T::MaxStrikes::get() {
|
|
Self::suspend_member(m);
|
|
}
|
|
amount
|
|
};
|
|
|
|
// Collect the voters who had a matching vote.
|
|
rewardees.extend(
|
|
votes
|
|
.into_iter()
|
|
.filter_map(|(v, m)| {
|
|
if v == matching_vote {
|
|
Some(m)
|
|
} else {
|
|
total_slash += bad_vote(m);
|
|
None
|
|
}
|
|
})
|
|
.cloned(),
|
|
);
|
|
|
|
if is_accepted {
|
|
total_approvals += approval_count;
|
|
total_payouts += value;
|
|
members.push(candidate.clone());
|
|
|
|
Self::pay_accepted_candidate(&candidate, value, kind, maturity);
|
|
|
|
// We track here the total_approvals so that every candidate has a unique
|
|
// range of numbers from 0 to `total_approvals` with length `approval_count`
|
|
// so each candidate is proportionally represented when selecting a
|
|
// "primary" below.
|
|
Some((candidate, total_approvals, value))
|
|
} else {
|
|
// Suspend Candidate
|
|
<SuspendedCandidates<T, I>>::insert(&candidate, (value, kind));
|
|
Self::deposit_event(Event::<T, I>::CandidateSuspended { candidate });
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// Clean up all votes.
|
|
#[allow(deprecated)]
|
|
<Votes<T, I>>::remove_all(None);
|
|
|
|
// Reward one of the voters who voted the right way.
|
|
if !total_slash.is_zero() {
|
|
if let Some(winner) = pick_item(&mut rng, &rewardees) {
|
|
// If we can't reward them, not much that can be done.
|
|
Self::bump_payout(winner, maturity, total_slash);
|
|
} else {
|
|
// Move the slashed amount back from payouts account to local treasury.
|
|
let res = T::Currency::transfer(
|
|
&Self::payouts(),
|
|
&Self::account_id(),
|
|
total_slash,
|
|
AllowDeath,
|
|
);
|
|
debug_assert!(res.is_ok());
|
|
}
|
|
}
|
|
|
|
// Fund the total payouts from the local treasury.
|
|
if !total_payouts.is_zero() {
|
|
// remove payout from pot and shift needed funds to the payout account.
|
|
pot = pot.saturating_sub(total_payouts);
|
|
|
|
// this should never fail since we ensure we can afford the payouts in a previous
|
|
// block, but there's not much we can do to recover if it fails anyway.
|
|
let res = T::Currency::transfer(
|
|
&Self::account_id(),
|
|
&Self::payouts(),
|
|
total_payouts,
|
|
AllowDeath,
|
|
);
|
|
debug_assert!(res.is_ok());
|
|
}
|
|
|
|
// if at least one candidate was accepted...
|
|
if !accepted.is_empty() {
|
|
// select one as primary, randomly chosen from the accepted, weighted by approvals.
|
|
// Choose a random number between 0 and `total_approvals`
|
|
let primary_point = pick_usize(&mut rng, total_approvals - 1);
|
|
// Find the zero bid or the user who falls on that point
|
|
let primary = accepted
|
|
.iter()
|
|
.find(|e| e.2.is_zero() || e.1 > primary_point)
|
|
.expect(
|
|
"e.1 of final item == total_approvals; \
|
|
worst case find will always return that item; qed",
|
|
)
|
|
.0
|
|
.clone();
|
|
|
|
let accounts = accepted.into_iter().map(|x| x.0).collect::<Vec<_>>();
|
|
|
|
// Then write everything back out, signal the changed membership and leave an event.
|
|
members.sort();
|
|
// NOTE: This may cause member length to surpass `MaxMembers`, but results in no
|
|
// consensus critical issues or side-effects. This is auto-correcting as members
|
|
// fall out of society.
|
|
<Members<T, I>>::put(&members[..]);
|
|
<Head<T, I>>::put(&primary);
|
|
|
|
T::MembershipChanged::change_members_sorted(&accounts, &[], members);
|
|
Self::deposit_event(Event::<T, I>::Inducted { primary, candidates: accounts });
|
|
}
|
|
|
|
// Bump the pot by at most PeriodSpend, but less if there's not very much left in our
|
|
// account.
|
|
let unaccounted = T::Currency::free_balance(&Self::account_id()).saturating_sub(pot);
|
|
pot += T::PeriodSpend::get().min(unaccounted / 2u8.into());
|
|
|
|
<Pot<T, I>>::put(&pot);
|
|
}
|
|
|
|
// Setup the candidates for the new intake
|
|
let candidates = Self::take_selected(members.len(), pot);
|
|
<Candidates<T, I>>::put(&candidates);
|
|
|
|
// Select sqrt(n) random members from the society and make them skeptics.
|
|
let pick_member =
|
|
|_| pick_item(&mut rng, &members[..]).expect("exited if members empty; qed").clone();
|
|
let skeptics = (0..members.len().integer_sqrt()).map(pick_member).collect::<Vec<_>>();
|
|
skeptics.iter().for_each(|skeptic| {
|
|
for Bid { who: c, .. } in candidates.iter() {
|
|
<Votes<T, I>>::insert(c, skeptic, Vote::Skeptic);
|
|
}
|
|
});
|
|
Self::deposit_event(Event::<T, I>::SkepticsChosen { skeptics });
|
|
}
|
|
|
|
/// Attempt to slash the payout of some member. Return the total amount that was deducted.
|
|
fn slash_payout(who: &T::AccountId, value: BalanceOf<T, I>) -> BalanceOf<T, I> {
|
|
let mut rest = value;
|
|
let mut payouts = <Payouts<T, I>>::get(who);
|
|
if !payouts.is_empty() {
|
|
let mut dropped = 0;
|
|
for (_, amount) in payouts.iter_mut() {
|
|
if let Some(new_rest) = rest.checked_sub(amount) {
|
|
// not yet totally slashed after this one; drop it completely.
|
|
rest = new_rest;
|
|
dropped += 1;
|
|
} else {
|
|
// whole slash is accounted for.
|
|
*amount -= rest;
|
|
rest = Zero::zero();
|
|
break
|
|
}
|
|
}
|
|
<Payouts<T, I>>::insert(who, &payouts[dropped..]);
|
|
}
|
|
value - rest
|
|
}
|
|
|
|
/// Bump the payout amount of `who`, to be unlocked at the given block number.
|
|
fn bump_payout(who: &T::AccountId, when: T::BlockNumber, value: BalanceOf<T, I>) {
|
|
if !value.is_zero() {
|
|
<Payouts<T, I>>::mutate(who, |payouts| {
|
|
match payouts.binary_search_by_key(&when, |x| x.0) {
|
|
Ok(index) => payouts[index].1 += value,
|
|
Err(index) => payouts.insert(index, (when, value)),
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Suspend a user, removing them from the member list.
|
|
fn suspend_member(who: &T::AccountId) {
|
|
if Self::remove_member(who).is_ok() {
|
|
<SuspendedMembers<T, I>>::insert(who, true);
|
|
<Strikes<T, I>>::remove(who);
|
|
Self::deposit_event(Event::<T, I>::MemberSuspended { member: who.clone() });
|
|
}
|
|
}
|
|
|
|
/// Pay an accepted candidate their bid value.
|
|
fn pay_accepted_candidate(
|
|
candidate: &T::AccountId,
|
|
value: BalanceOf<T, I>,
|
|
kind: BidKind<T::AccountId, BalanceOf<T, I>>,
|
|
maturity: T::BlockNumber,
|
|
) {
|
|
let value = match kind {
|
|
BidKind::Deposit(deposit) => {
|
|
// In the case that a normal deposit bid is accepted we unreserve
|
|
// the deposit.
|
|
let err_amount = T::Currency::unreserve(candidate, deposit);
|
|
debug_assert!(err_amount.is_zero());
|
|
value
|
|
},
|
|
BidKind::Vouch(voucher, tip) => {
|
|
// Check that the voucher is still vouching, else some other logic may have removed
|
|
// their status.
|
|
if <Vouching<T, I>>::take(&voucher) == Some(VouchingStatus::Vouching) {
|
|
// In the case that a vouched-for bid is accepted we unset the
|
|
// vouching status and transfer the tip over to the voucher.
|
|
Self::bump_payout(&voucher, maturity, tip.min(value));
|
|
value.saturating_sub(tip)
|
|
} else {
|
|
value
|
|
}
|
|
},
|
|
};
|
|
|
|
Self::bump_payout(candidate, maturity, value);
|
|
}
|
|
|
|
/// End the current challenge period and start a new one.
|
|
fn rotate_challenge(members: &mut Vec<T::AccountId>) {
|
|
// Assume there are members, else don't run this logic.
|
|
if !members.is_empty() {
|
|
// End current defender rotation
|
|
if let Some(defender) = Self::defender() {
|
|
let mut approval_count = 0;
|
|
let mut rejection_count = 0;
|
|
// Tallies total number of approve and reject votes for the defender.
|
|
members.iter().filter_map(<DefenderVotes<T, I>>::take).for_each(|v| match v {
|
|
Vote::Approve => approval_count += 1,
|
|
_ => rejection_count += 1,
|
|
});
|
|
|
|
if approval_count <= rejection_count {
|
|
// User has failed the challenge
|
|
Self::suspend_member(&defender);
|
|
*members = Self::members();
|
|
}
|
|
|
|
// Clean up all votes.
|
|
#[allow(deprecated)]
|
|
<DefenderVotes<T, I>>::remove_all(None);
|
|
}
|
|
|
|
// Avoid challenging if there's only two members since we never challenge the Head or
|
|
// the Founder.
|
|
if members.len() > 2 {
|
|
// Start a new defender rotation
|
|
let phrase = b"society_challenge";
|
|
// we'll need a random seed here.
|
|
// TODO: deal with randomness freshness
|
|
// https://github.com/paritytech/substrate/issues/8312
|
|
let (seed, _) = T::Randomness::random(phrase);
|
|
// seed needs to be guaranteed to be 32 bytes.
|
|
let seed = <[u8; 32]>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
|
|
.expect("input is padded with zeroes; qed");
|
|
let mut rng = ChaChaRng::from_seed(seed);
|
|
let chosen = pick_item(&mut rng, &members[1..members.len() - 1])
|
|
.expect("exited if members empty; qed");
|
|
<Defender<T, I>>::put(&chosen);
|
|
Self::deposit_event(Event::<T, I>::Challenged { member: chosen.clone() });
|
|
} else {
|
|
<Defender<T, I>>::kill();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The account ID of the treasury pot.
|
|
///
|
|
/// This actually does computation. If you need to keep using it, then make sure you cache the
|
|
/// value and only call this once.
|
|
pub fn account_id() -> T::AccountId {
|
|
T::PalletId::get().into_account_truncating()
|
|
}
|
|
|
|
/// The account ID of the payouts pot. This is where payouts are made from.
|
|
///
|
|
/// This actually does computation. If you need to keep using it, then make sure you cache the
|
|
/// value and only call this once.
|
|
pub fn payouts() -> T::AccountId {
|
|
T::PalletId::get().into_sub_account_truncating(b"payouts")
|
|
}
|
|
|
|
/// Return the duration of the lock, in blocks, with the given number of members.
|
|
///
|
|
/// This is a rather opaque calculation based on the formula here:
|
|
/// https://www.desmos.com/calculator/9itkal1tce
|
|
fn lock_duration(x: u32) -> T::BlockNumber {
|
|
let lock_pc = 100 - 50_000 / (x + 500);
|
|
Percent::from_percent(lock_pc as u8) * T::MaxLockDuration::get()
|
|
}
|
|
|
|
/// Get a selection of bidding accounts such that the total bids is no greater than `Pot` and
|
|
/// the number of bids would not surpass `MaxMembers` if all were accepted.
|
|
///
|
|
/// May be empty.
|
|
pub fn take_selected(
|
|
members_len: usize,
|
|
pot: BalanceOf<T, I>,
|
|
) -> Vec<Bid<T::AccountId, BalanceOf<T, I>>> {
|
|
let max_members = MaxMembers::<T, I>::get() as usize;
|
|
let mut max_selections: usize =
|
|
(T::MaxCandidateIntake::get() as usize).min(max_members.saturating_sub(members_len));
|
|
|
|
if max_selections > 0 {
|
|
// Get the number of left-most bidders whose bids add up to less than `pot`.
|
|
let mut bids = <Bids<T, I>>::get();
|
|
|
|
// The list of selected candidates
|
|
let mut selected = Vec::new();
|
|
|
|
if bids.len() > 0 {
|
|
// Can only select at most the length of bids
|
|
max_selections = max_selections.min(bids.len());
|
|
// Number of selected bids so far
|
|
let mut count = 0;
|
|
// Check if we have already selected a candidate with zero bid
|
|
let mut zero_selected = false;
|
|
// A running total of the cost to onboard these bids
|
|
let mut total_cost: BalanceOf<T, I> = Zero::zero();
|
|
|
|
bids.retain(|bid| {
|
|
if count < max_selections {
|
|
// Handle zero bids. We only want one of them.
|
|
if bid.value.is_zero() {
|
|
// Select only the first zero bid
|
|
if !zero_selected {
|
|
selected.push(bid.clone());
|
|
zero_selected = true;
|
|
count += 1;
|
|
return false
|
|
}
|
|
} else {
|
|
total_cost += bid.value;
|
|
// Select only as many users as the pot can support.
|
|
if total_cost <= pot {
|
|
selected.push(bid.clone());
|
|
count += 1;
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
true
|
|
});
|
|
|
|
// No need to reset Bids if we're not taking anything.
|
|
if count > 0 {
|
|
<Bids<T, I>>::put(bids);
|
|
}
|
|
}
|
|
selected
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Pallet<T, I> {
|
|
fn on_nonzero_unbalanced(amount: NegativeImbalanceOf<T, I>) {
|
|
let numeric_amount = amount.peek();
|
|
|
|
// Must resolve into existing but better to be safe.
|
|
let _ = T::Currency::resolve_creating(&Self::account_id(), amount);
|
|
|
|
Self::deposit_event(Event::<T, I>::Deposit { value: numeric_amount });
|
|
}
|
|
}
|