// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // 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. //! # Alliance Pezpallet //! //! The Alliance Pezpallet provides a collective that curates a list of accounts and URLs, deemed by //! the voting members to be unscrupulous actors. The Alliance //! //! - provides a set of ethics against bad behavior, and //! - provides recognition and influence for those teams that contribute something back to the //! ecosystem. //! //! ## Overview //! //! The network initializes the Alliance via a Root call. After that, anyone with an approved //! identity and website can join as an Ally. The `MembershipManager` origin can elevate Allies to //! Fellows, giving them voting rights within the Alliance. //! //! Voting members of the Alliance maintain a list of accounts and websites. Members can also vote //! to update the Alliance's rule and make announcements. //! //! ### Terminology //! //! - Rule: The IPFS CID (hash) of the Alliance rules for the community to read and the Alliance //! members to enforce. Similar to a Charter or Code of Conduct. //! - Announcement: An IPFS CID of some content that the Alliance want to announce. //! - Member: An account that is already in the group of the Alliance, including two types: Fellow, //! or Ally. A member can also be kicked by the `MembershipManager` origin or retire by itself. //! - Fellow: An account who is elevated from Ally by other Fellows. //! - Ally: An account who would like to join the Alliance. To become a voting member (Fellow), it //! will need approval from the `MembershipManager` origin. Any account can join as an Ally either //! by placing a deposit or by nomination from a voting member. //! - Unscrupulous List: A list of bad websites and addresses; items can be added or removed by //! voting members. //! //! ## Interface //! //! ### Dispatchable Functions //! //! #### For General Users //! //! - `join_alliance` - Join the Alliance as an Ally. This requires a slashable deposit. //! //! #### For Members (All) //! //! - `give_retirement_notice` - Give a retirement notice and start a retirement period required to //! pass in order to retire. //! - `retire` - Retire from the Alliance and release the caller's deposit. //! //! #### For Voting Members //! //! - `propose` - Propose a motion. //! - `vote` - Vote on a motion. //! - `close` - Close a motion with enough votes or that has expired. //! - `set_rule` - Initialize or update the Alliance's rule by IPFS CID. //! - `announce` - Make announcement by IPFS CID. //! - `nominate_ally` - Nominate a non-member to become an Ally, without deposit. //! - `elevate_ally` - Approve an ally to become a Fellow. //! - `kick_member` - Kick a member and slash its deposit. //! - `add_unscrupulous_items` - Add some items, either accounts or websites, to the list of //! unscrupulous items. //! - `remove_unscrupulous_items` - Remove some items from the list of unscrupulous items. //! - `abdicate_fellow_status` - Abdicate one's voting rights, demoting themself to Ally. //! //! #### Root Calls //! //! - `init_members` - Initialize the Alliance, onboard fellows and allies. //! - `disband` - Disband the Alliance, remove all active members and unreserve deposits. #![cfg_attr(not(feature = "std"), no_std)] #[cfg(test)] mod mock; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod migration; mod types; pub mod weights; extern crate alloc; use alloc::{boxed::Box, vec, vec::Vec}; use codec::{Decode, Encode, MaxEncodedLen}; use pezframe_support::pezpallet_prelude::*; use pezframe_system::pezpallet_prelude::*; use pezsp_runtime::{ traits::{Dispatchable, Saturating, StaticLookup, Zero}, DispatchError, RuntimeDebug, }; use pezframe_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, PostDispatchInfo}, ensure, traits::{ ChangeMembers, Currency, Get, InitializeMembers, IsSubType, OnUnbalanced, ReservableCurrency, }, weights::Weight, }; use scale_info::TypeInfo; pub use pezpallet::*; pub use types::*; pub use weights::*; /// The log target of this pezpallet. pub const LOG_TARGET: &str = "runtime::alliance"; /// Simple index type for proposal counting. pub type ProposalIndex = u32; type UrlOf = BoundedVec>::MaxWebsiteUrlLength>; type BalanceOf = <>::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <>::Currency as Currency< ::AccountId, >>::NegativeImbalance; /// Interface required for identity verification. pub trait IdentityVerifier { /// Function that returns whether an account has the required identities registered with the /// identity provider. fn has_required_identities(who: &AccountId) -> bool; /// Whether an account has been deemed "good" by the provider. fn has_good_judgement(who: &AccountId) -> bool; /// If the identity provider allows sub-accounts, provide the super of an account. Should /// return `None` if the provider does not allow sub-accounts or if the account is not a sub. fn super_account_id(who: &AccountId) -> Option; } /// The non-provider. Imposes no restrictions on account identity. impl IdentityVerifier for () { fn has_required_identities(_who: &AccountId) -> bool { true } fn has_good_judgement(_who: &AccountId) -> bool { true } fn super_account_id(_who: &AccountId) -> Option { None } } /// The provider of a collective action interface, for example an instance of /// `pezpallet-collective`. pub trait ProposalProvider { /// Add a new proposal. /// Returns a proposal length and active proposals count if successful. fn propose_proposal( who: AccountId, threshold: u32, proposal: Box, length_bound: u32, ) -> Result<(u32, u32), DispatchError>; /// Add an aye or nay vote for the sender to the given proposal. /// Returns true if the sender votes first time if successful. fn vote_proposal( who: AccountId, proposal: Hash, index: ProposalIndex, approve: bool, ) -> Result; /// Close a proposal that is either approved, disapproved, or whose voting period has ended. fn close_proposal( proposal_hash: Hash, index: ProposalIndex, proposal_weight_bound: Weight, length_bound: u32, ) -> DispatchResultWithPostInfo; /// Return a proposal of the given hash. fn proposal_of(proposal_hash: Hash) -> Option; } /// The various roles that a member can hold. #[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] pub enum MemberRole { Fellow, Ally, Retiring, } /// The type of item that may be deemed unscrupulous. #[derive( Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] pub enum UnscrupulousItem { AccountId(AccountId), Website(Url), } type UnscrupulousItemOf = UnscrupulousItem<::AccountId, UrlOf>; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; #[pezpallet::pezpallet] #[pezpallet::storage_version(migration::STORAGE_VERSION)] pub struct Pezpallet(PhantomData<(T, I)>); #[pezpallet::config] pub trait Config: pezframe_system::Config { /// The overarching event type. #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The runtime call dispatch type. type Proposal: Parameter + Dispatchable + From> + From> + GetDispatchInfo + IsSubType> + IsType<::RuntimeCall>; /// Origin for admin-level operations, like setting the Alliance's rules. type AdminOrigin: EnsureOrigin; /// Origin that manages entry and forcible discharge from the Alliance. type MembershipManager: EnsureOrigin; /// Origin for making announcements and adding/removing unscrupulous items. type AnnouncementOrigin: EnsureOrigin; /// The currency used for deposits. type Currency: ReservableCurrency; /// What to do with slashed funds. type Slashed: OnUnbalanced>; /// What to do with initial voting members of the Alliance. type InitializeMembers: InitializeMembers; /// What to do when a member has been added or removed. type MembershipChanged: ChangeMembers; /// The identity verifier of an Alliance member. type IdentityVerifier: IdentityVerifier; /// The provider of the proposal operation. type ProposalProvider: ProposalProvider; /// Maximum number of proposals allowed to be active in parallel. type MaxProposals: Get; /// The maximum number of Fellows supported by the pezpallet. Used for weight estimation. /// /// NOTE: /// + Benchmarks will need to be re-run and weights adjusted if this changes. /// + This pezpallet assumes that dependencies keep to the limit without enforcing it. type MaxFellows: Get; /// The maximum number of Allies supported by the pezpallet. Used for weight estimation. /// /// NOTE: /// + Benchmarks will need to be re-run and weights adjusted if this changes. /// + This pezpallet assumes that dependencies keep to the limit without enforcing it. type MaxAllies: Get; /// The maximum number of the unscrupulous items supported by the pezpallet. #[pezpallet::constant] type MaxUnscrupulousItems: Get; /// The maximum length of a website URL. #[pezpallet::constant] type MaxWebsiteUrlLength: Get; /// The deposit required for submitting candidacy. #[pezpallet::constant] type AllyDeposit: Get>; /// The maximum number of announcements. #[pezpallet::constant] type MaxAnnouncementsCount: Get; /// The maximum number of members per member role. #[pezpallet::constant] type MaxMembersCount: Get; /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; /// The number of blocks a member must wait between giving a retirement notice and retiring. /// Supposed to be greater than time required to `kick_member`. type RetirementPeriod: Get>; } #[pezpallet::error] pub enum Error { /// The Alliance has not been initialized yet, therefore accounts cannot join it. AllianceNotYetInitialized, /// The Alliance has been initialized, therefore cannot be initialized again. AllianceAlreadyInitialized, /// Account is already a member. AlreadyMember, /// Account is not a member. NotMember, /// Account is not an ally. NotAlly, /// Account does not have voting rights. NoVotingRights, /// Account is already an elevated (fellow) member. AlreadyElevated, /// Item is already listed as unscrupulous. AlreadyUnscrupulous, /// Account has been deemed unscrupulous by the Alliance and is not welcome to join or be /// nominated. AccountNonGrata, /// Item has not been deemed unscrupulous. NotListedAsUnscrupulous, /// The number of unscrupulous items exceeds `MaxUnscrupulousItems`. TooManyUnscrupulousItems, /// Length of website URL exceeds `MaxWebsiteUrlLength`. TooLongWebsiteUrl, /// Balance is insufficient for the required deposit. InsufficientFunds, /// The account's identity does not have display field and website field. WithoutRequiredIdentityFields, /// The account's identity has no good judgement. WithoutGoodIdentityJudgement, /// The proposal hash is not found. MissingProposalHash, /// The announcement is not found. MissingAnnouncement, /// Number of members exceeds `MaxMembersCount`. TooManyMembers, /// Number of announcements exceeds `MaxAnnouncementsCount`. TooManyAnnouncements, /// Invalid witness data given. BadWitness, /// Account already gave retirement notice AlreadyRetiring, /// Account did not give a retirement notice required to retire. RetirementNoticeNotGiven, /// Retirement period has not passed. RetirementPeriodNotPassed, /// Fellows must be provided to initialize the Alliance. FellowsMissing, } #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// A new rule has been set. NewRuleSet { rule: Cid }, /// A new announcement has been proposed. Announced { announcement: Cid }, /// An on-chain announcement has been removed. AnnouncementRemoved { announcement: Cid }, /// Some accounts have been initialized as members (fellows/allies). MembersInitialized { fellows: Vec, allies: Vec }, /// An account has been added as an Ally and reserved its deposit. NewAllyJoined { ally: T::AccountId, nominator: Option, reserved: Option>, }, /// An ally has been elevated to Fellow. AllyElevated { ally: T::AccountId }, /// A member gave retirement notice and their retirement period started. MemberRetirementPeriodStarted { member: T::AccountId }, /// A member has retired with its deposit unreserved. MemberRetired { member: T::AccountId, unreserved: Option> }, /// A member has been kicked out with its deposit slashed. MemberKicked { member: T::AccountId, slashed: Option> }, /// Accounts or websites have been added into the list of unscrupulous items. UnscrupulousItemAdded { items: Vec> }, /// Accounts or websites have been removed from the list of unscrupulous items. UnscrupulousItemRemoved { items: Vec> }, /// Alliance disbanded. Includes number deleted members and unreserved deposits. AllianceDisbanded { fellow_members: u32, ally_members: u32, unreserved: u32 }, /// A Fellow abdicated their voting rights. They are now an Ally. FellowAbdicated { fellow: T::AccountId }, } #[pezpallet::genesis_config] #[derive(pezframe_support::DefaultNoBound)] pub struct GenesisConfig, I: 'static = ()> { pub fellows: Vec, pub allies: Vec, #[serde(skip)] pub phantom: PhantomData<(T, I)>, } #[pezpallet::hooks] impl, I: 'static> Hooks> for Pezpallet { #[cfg(feature = "try-runtime")] fn try_state(_n: BlockNumberFor) -> Result<(), pezsp_runtime::TryRuntimeError> { Self::do_try_state() } } #[pezpallet::genesis_build] impl, I: 'static> BuildGenesisConfig for GenesisConfig { fn build(&self) { for m in self.fellows.iter().chain(self.allies.iter()) { assert!( Pezpallet::::has_identity(m).is_ok(), "Member does not set identity!" ); } if !self.fellows.is_empty() { assert!( !Pezpallet::::has_member(MemberRole::Fellow), "Fellows are already initialized!" ); let members: BoundedVec = self.fellows.clone().try_into().expect("Too many genesis fellows"); Members::::insert(MemberRole::Fellow, members); } if !self.allies.is_empty() { assert!( !Pezpallet::::has_member(MemberRole::Ally), "Allies are already initialized!" ); assert!( !self.fellows.is_empty(), "Fellows must be provided to initialize the Alliance" ); let members: BoundedVec = self.allies.clone().try_into().expect("Too many genesis allies"); Members::::insert(MemberRole::Ally, members); } T::InitializeMembers::initialize_members(self.fellows.as_slice()) } } /// The IPFS CID of the alliance rule. /// Fellows can propose a new rule with a super-majority. #[pezpallet::storage] pub type Rule, I: 'static = ()> = StorageValue<_, Cid, OptionQuery>; /// The current IPFS CIDs of any announcements. #[pezpallet::storage] pub type Announcements, I: 'static = ()> = StorageValue<_, BoundedVec, ValueQuery>; /// Maps members to their candidacy deposit. #[pezpallet::storage] pub type DepositOf, I: 'static = ()> = StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf, OptionQuery>; /// Maps member type to members of each type. #[pezpallet::storage] pub type Members, I: 'static = ()> = StorageMap< _, Twox64Concat, MemberRole, BoundedVec, ValueQuery, >; /// A set of members who gave a retirement notice. They can retire after the end of retirement /// period stored as a future block number. #[pezpallet::storage] pub type RetiringMembers, I: 'static = ()> = StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor, OptionQuery>; /// The current list of accounts deemed unscrupulous. These accounts non grata cannot submit /// candidacy. #[pezpallet::storage] pub type UnscrupulousAccounts, I: 'static = ()> = StorageValue<_, BoundedVec, ValueQuery>; /// The current list of websites deemed unscrupulous. #[pezpallet::storage] pub type UnscrupulousWebsites, I: 'static = ()> = StorageValue<_, BoundedVec, T::MaxUnscrupulousItems>, ValueQuery>; #[pezpallet::call(weight(>::WeightInfo))] impl, I: 'static> Pezpallet { /// Add a new proposal to be voted on. /// /// Must be called by a Fellow. #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::propose_proposed( *length_bound, // B T::MaxFellows::get(), // M T::MaxProposals::get(), // P2 ))] pub fn propose( origin: OriginFor, #[pezpallet::compact] threshold: u32, proposal: Box<>::Proposal>, #[pezpallet::compact] length_bound: u32, ) -> DispatchResult { let proposer = ensure_signed(origin)?; ensure!(Self::has_voting_rights(&proposer), Error::::NoVotingRights); T::ProposalProvider::propose_proposal(proposer, threshold, proposal, length_bound)?; Ok(()) } /// Add an aye or nay vote for the sender to the given proposal. /// /// Must be called by a Fellow. #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::vote(T::MaxFellows::get()))] pub fn vote( origin: OriginFor, proposal: T::Hash, #[pezpallet::compact] index: ProposalIndex, approve: bool, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(Self::has_voting_rights(&who), Error::::NoVotingRights); T::ProposalProvider::vote_proposal(who, proposal, index, approve)?; Ok(()) } // Index 2 was `close_old_weight`; it was removed due to weights v1 deprecation. /// Initialize the Alliance, onboard fellows and allies. /// /// The Alliance must be empty, and the call must provide some founding members. /// /// Must be called by the Root origin. #[pezpallet::call_index(3)] #[pezpallet::weight(T::WeightInfo::init_members( fellows.len() as u32, allies.len() as u32, ))] pub fn init_members( origin: OriginFor, fellows: Vec, allies: Vec, ) -> DispatchResult { ensure_root(origin)?; ensure!(!fellows.is_empty(), Error::::FellowsMissing); ensure!(!Self::is_initialized(), Error::::AllianceAlreadyInitialized); let mut fellows: BoundedVec = fellows.try_into().map_err(|_| Error::::TooManyMembers)?; let mut allies: BoundedVec = allies.try_into().map_err(|_| Error::::TooManyMembers)?; for member in fellows.iter().chain(allies.iter()) { Self::has_identity(member)?; } fellows.sort(); Members::::insert(&MemberRole::Fellow, fellows.clone()); allies.sort(); Members::::insert(&MemberRole::Ally, allies.clone()); let mut voteable_members = fellows.clone(); voteable_members.sort(); T::InitializeMembers::initialize_members(&voteable_members); log::debug!( target: LOG_TARGET, "Initialize alliance fellows: {:?}, allies: {:?}", fellows, allies ); Self::deposit_event(Event::MembersInitialized { fellows: fellows.into(), allies: allies.into(), }); Ok(()) } /// Disband the Alliance, remove all active members and unreserve deposits. /// /// Witness data must be set. #[pezpallet::call_index(4)] #[pezpallet::weight(T::WeightInfo::disband( witness.fellow_members, witness.ally_members, witness.fellow_members.saturating_add(witness.ally_members), ))] pub fn disband( origin: OriginFor, witness: DisbandWitness, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; ensure!(!witness.is_zero(), Error::::BadWitness); ensure!( Self::voting_members_count() <= witness.fellow_members, Error::::BadWitness ); ensure!(Self::ally_members_count() <= witness.ally_members, Error::::BadWitness); ensure!(Self::is_initialized(), Error::::AllianceNotYetInitialized); let voting_members = Self::voting_members(); T::MembershipChanged::change_members_sorted(&[], &voting_members, &[]); let ally_members = Self::members_of(MemberRole::Ally); let mut unreserve_count: u32 = 0; for member in voting_members.iter().chain(ally_members.iter()) { if let Some(deposit) = DepositOf::::take(&member) { let err_amount = T::Currency::unreserve(&member, deposit); debug_assert!(err_amount.is_zero()); unreserve_count += 1; } } Members::::remove(&MemberRole::Fellow); Members::::remove(&MemberRole::Ally); Self::deposit_event(Event::AllianceDisbanded { fellow_members: voting_members.len() as u32, ally_members: ally_members.len() as u32, unreserved: unreserve_count, }); Ok(Some(T::WeightInfo::disband( voting_members.len() as u32, ally_members.len() as u32, unreserve_count, )) .into()) } /// Set a new IPFS CID to the alliance rule. #[pezpallet::call_index(5)] pub fn set_rule(origin: OriginFor, rule: Cid) -> DispatchResult { T::AdminOrigin::ensure_origin(origin)?; Rule::::put(&rule); Self::deposit_event(Event::NewRuleSet { rule }); Ok(()) } /// Make an announcement of a new IPFS CID about alliance issues. #[pezpallet::call_index(6)] pub fn announce(origin: OriginFor, announcement: Cid) -> DispatchResult { T::AnnouncementOrigin::ensure_origin(origin)?; let mut announcements = >::get(); announcements .try_push(announcement.clone()) .map_err(|_| Error::::TooManyAnnouncements)?; >::put(announcements); Self::deposit_event(Event::Announced { announcement }); Ok(()) } /// Remove an announcement. #[pezpallet::call_index(7)] pub fn remove_announcement(origin: OriginFor, announcement: Cid) -> DispatchResult { T::AnnouncementOrigin::ensure_origin(origin)?; let mut announcements = >::get(); let pos = announcements .binary_search(&announcement) .ok() .ok_or(Error::::MissingAnnouncement)?; announcements.remove(pos); >::put(announcements); Self::deposit_event(Event::AnnouncementRemoved { announcement }); Ok(()) } /// Submit oneself for candidacy. A fixed deposit is reserved. #[pezpallet::call_index(8)] pub fn join_alliance(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; // We don't want anyone to join as an Ally before the Alliance has been initialized via // Root call. The reasons are two-fold: // // 1. There is no `Rule` or admission criteria, so the joiner would be an ally to // nought, and // 2. It adds complexity to the initialization, namely deciding to overwrite accounts // that already joined as an Ally. ensure!(Self::is_initialized(), Error::::AllianceNotYetInitialized); // Unscrupulous accounts are non grata. ensure!(!Self::is_unscrupulous_account(&who), Error::::AccountNonGrata); ensure!(!Self::is_member(&who), Error::::AlreadyMember); // check user self or parent should has verified identity to reuse display name and // website. Self::has_identity(&who)?; let deposit = T::AllyDeposit::get(); T::Currency::reserve(&who, deposit).map_err(|_| Error::::InsufficientFunds)?; >::insert(&who, deposit); Self::add_member(&who, MemberRole::Ally)?; Self::deposit_event(Event::NewAllyJoined { ally: who, nominator: None, reserved: Some(deposit), }); Ok(()) } /// A Fellow can nominate someone to join the alliance as an Ally. There is no deposit /// required from the nominator or nominee. #[pezpallet::call_index(9)] pub fn nominate_ally(origin: OriginFor, who: AccountIdLookupOf) -> DispatchResult { let nominator = ensure_signed(origin)?; ensure!(Self::has_voting_rights(&nominator), Error::::NoVotingRights); let who = T::Lookup::lookup(who)?; // Individual voting members cannot nominate accounts non grata. ensure!(!Self::is_unscrupulous_account(&who), Error::::AccountNonGrata); ensure!(!Self::is_member(&who), Error::::AlreadyMember); // check user self or parent should has verified identity to reuse display name and // website. Self::has_identity(&who)?; Self::add_member(&who, MemberRole::Ally)?; Self::deposit_event(Event::NewAllyJoined { ally: who, nominator: Some(nominator), reserved: None, }); Ok(()) } /// Elevate an Ally to Fellow. #[pezpallet::call_index(10)] pub fn elevate_ally(origin: OriginFor, ally: AccountIdLookupOf) -> DispatchResult { T::MembershipManager::ensure_origin(origin)?; let ally = T::Lookup::lookup(ally)?; ensure!(Self::is_ally(&ally), Error::::NotAlly); ensure!(!Self::has_voting_rights(&ally), Error::::AlreadyElevated); Self::remove_member(&ally, MemberRole::Ally)?; Self::add_member(&ally, MemberRole::Fellow)?; Self::deposit_event(Event::AllyElevated { ally }); Ok(()) } /// As a member, give a retirement notice and start a retirement period required to pass in /// order to retire. #[pezpallet::call_index(11)] pub fn give_retirement_notice(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; let role = Self::member_role_of(&who).ok_or(Error::::NotMember)?; ensure!(role.ne(&MemberRole::Retiring), Error::::AlreadyRetiring); Self::remove_member(&who, role)?; Self::add_member(&who, MemberRole::Retiring)?; >::insert( &who, pezframe_system::Pezpallet::::block_number() .saturating_add(T::RetirementPeriod::get()), ); Self::deposit_event(Event::MemberRetirementPeriodStarted { member: who }); Ok(()) } /// As a member, retire from the Alliance and unreserve the deposit. /// /// This can only be done once you have called `give_retirement_notice` and the /// `RetirementPeriod` has passed. #[pezpallet::call_index(12)] pub fn retire(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; let retirement_period_end = RetiringMembers::::get(&who) .ok_or(Error::::RetirementNoticeNotGiven)?; ensure!( pezframe_system::Pezpallet::::block_number() >= retirement_period_end, Error::::RetirementPeriodNotPassed ); Self::remove_member(&who, MemberRole::Retiring)?; >::remove(&who); let deposit = DepositOf::::take(&who); if let Some(deposit) = deposit { let err_amount = T::Currency::unreserve(&who, deposit); debug_assert!(err_amount.is_zero()); } Self::deposit_event(Event::MemberRetired { member: who, unreserved: deposit }); Ok(()) } /// Kick a member from the Alliance and slash its deposit. #[pezpallet::call_index(13)] pub fn kick_member(origin: OriginFor, who: AccountIdLookupOf) -> DispatchResult { T::MembershipManager::ensure_origin(origin)?; let member = T::Lookup::lookup(who)?; let role = Self::member_role_of(&member).ok_or(Error::::NotMember)?; Self::remove_member(&member, role)?; let deposit = DepositOf::::take(member.clone()); if let Some(deposit) = deposit { T::Slashed::on_unbalanced(T::Currency::slash_reserved(&member, deposit).0); } Self::deposit_event(Event::MemberKicked { member, slashed: deposit }); Ok(()) } /// Add accounts or websites to the list of unscrupulous items. #[pezpallet::call_index(14)] #[pezpallet::weight(T::WeightInfo::add_unscrupulous_items(items.len() as u32, T::MaxWebsiteUrlLength::get()))] pub fn add_unscrupulous_items( origin: OriginFor, items: Vec>, ) -> DispatchResult { T::AnnouncementOrigin::ensure_origin(origin)?; let mut accounts = vec![]; let mut webs = vec![]; for info in items.iter() { ensure!(!Self::is_unscrupulous(info), Error::::AlreadyUnscrupulous); match info { UnscrupulousItem::AccountId(who) => accounts.push(who.clone()), UnscrupulousItem::Website(url) => { ensure!( url.len() as u32 <= T::MaxWebsiteUrlLength::get(), Error::::TooLongWebsiteUrl ); webs.push(url.clone()); }, } } Self::do_add_unscrupulous_items(&mut accounts, &mut webs)?; Self::deposit_event(Event::UnscrupulousItemAdded { items }); Ok(()) } /// Deem some items no longer unscrupulous. #[pezpallet::call_index(15)] #[pezpallet::weight(>::WeightInfo::remove_unscrupulous_items( items.len() as u32, T::MaxWebsiteUrlLength::get() ))] pub fn remove_unscrupulous_items( origin: OriginFor, items: Vec>, ) -> DispatchResult { T::AnnouncementOrigin::ensure_origin(origin)?; let mut accounts = vec![]; let mut webs = vec![]; for info in items.iter() { ensure!(Self::is_unscrupulous(info), Error::::NotListedAsUnscrupulous); match info { UnscrupulousItem::AccountId(who) => accounts.push(who.clone()), UnscrupulousItem::Website(url) => webs.push(url.clone()), } } Self::do_remove_unscrupulous_items(&mut accounts, &mut webs)?; Self::deposit_event(Event::UnscrupulousItemRemoved { items }); Ok(()) } /// Close a vote that is either approved, disapproved, or whose voting period has ended. /// /// Must be called by a Fellow. #[pezpallet::call_index(16)] #[pezpallet::weight({ let b = *length_bound; let m = T::MaxFellows::get(); let p1 = *proposal_weight_bound; let p2 = T::MaxProposals::get(); T::WeightInfo::close_early_approved(b, m, p2) .max(T::WeightInfo::close_early_disapproved(m, p2)) .max(T::WeightInfo::close_approved(b, m, p2)) .max(T::WeightInfo::close_disapproved(m, p2)) .saturating_add(p1) })] pub fn close( origin: OriginFor, proposal_hash: T::Hash, #[pezpallet::compact] index: ProposalIndex, proposal_weight_bound: Weight, #[pezpallet::compact] length_bound: u32, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; ensure!(Self::has_voting_rights(&who), Error::::NoVotingRights); Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound) } /// Abdicate one's position as a voting member and just be an Ally. May be used by Fellows /// who do not want to leave the Alliance but do not have the capacity to participate /// operationally for some time. #[pezpallet::call_index(17)] pub fn abdicate_fellow_status(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; let role = Self::member_role_of(&who).ok_or(Error::::NotMember)?; // Not applicable to members who are retiring or who are already Allies. ensure!(Self::has_voting_rights(&who), Error::::NoVotingRights); Self::remove_member(&who, role)?; Self::add_member(&who, MemberRole::Ally)?; Self::deposit_event(Event::FellowAbdicated { fellow: who }); Ok(()) } } } impl, I: 'static> Pezpallet { /// Check if the Alliance has been initialized. fn is_initialized() -> bool { Self::has_member(MemberRole::Fellow) || Self::has_member(MemberRole::Ally) } /// Check if a given role has any members. fn has_member(role: MemberRole) -> bool { Members::::decode_len(role).unwrap_or_default() > 0 } /// Look up the role, if any, of an account. fn member_role_of(who: &T::AccountId) -> Option { Members::::iter() .find_map(|(r, members)| if members.contains(who) { Some(r) } else { None }) } /// Check if a user is a alliance member. pub fn is_member(who: &T::AccountId) -> bool { Self::member_role_of(who).is_some() } /// Check if an account has a given role. pub fn is_member_of(who: &T::AccountId, role: MemberRole) -> bool { Members::::get(role).contains(&who) } /// Check if an account is an Ally. fn is_ally(who: &T::AccountId) -> bool { Self::is_member_of(who, MemberRole::Ally) } /// Check if a member has voting rights. fn has_voting_rights(who: &T::AccountId) -> bool { Self::is_member_of(who, MemberRole::Fellow) } /// Count of ally members. fn ally_members_count() -> u32 { Members::::decode_len(MemberRole::Ally).unwrap_or(0) as u32 } /// Count of all members who have voting rights. fn voting_members_count() -> u32 { Members::::decode_len(MemberRole::Fellow).unwrap_or(0) as u32 } /// Get all members of a given role. fn members_of(role: MemberRole) -> Vec { Members::::get(role).into_inner() } /// Collect all members who have voting rights into one list. fn voting_members() -> Vec { Self::members_of(MemberRole::Fellow) } /// Add a user to the sorted alliance member set. fn add_member(who: &T::AccountId, role: MemberRole) -> DispatchResult { >::try_mutate(role, |members| -> DispatchResult { let pos = members.binary_search(who).err().ok_or(Error::::AlreadyMember)?; members .try_insert(pos, who.clone()) .map_err(|_| Error::::TooManyMembers)?; Ok(()) })?; if role == MemberRole::Fellow { let members = Self::voting_members(); T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members[..]); } Ok(()) } /// Remove a user from the alliance member set. fn remove_member(who: &T::AccountId, role: MemberRole) -> DispatchResult { >::try_mutate(role, |members| -> DispatchResult { let pos = members.binary_search(who).ok().ok_or(Error::::NotMember)?; members.remove(pos); Ok(()) })?; if role == MemberRole::Fellow { let members = Self::voting_members(); T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]); } Ok(()) } /// Check if an item is listed as unscrupulous. fn is_unscrupulous(info: &UnscrupulousItemOf) -> bool { match info { UnscrupulousItem::Website(url) => >::get().contains(url), UnscrupulousItem::AccountId(who) => >::get().contains(who), } } /// Check if an account is listed as unscrupulous. fn is_unscrupulous_account(who: &T::AccountId) -> bool { >::get().contains(who) } /// Add item to the unscrupulous list. fn do_add_unscrupulous_items( new_accounts: &mut Vec, new_webs: &mut Vec>, ) -> DispatchResult { if !new_accounts.is_empty() { >::try_mutate(|accounts| -> DispatchResult { accounts .try_append(new_accounts) .map_err(|_| Error::::TooManyUnscrupulousItems)?; accounts.sort(); Ok(()) })?; } if !new_webs.is_empty() { >::try_mutate(|webs| -> DispatchResult { webs.try_append(new_webs).map_err(|_| Error::::TooManyUnscrupulousItems)?; webs.sort(); Ok(()) })?; } Ok(()) } /// Remove item from the unscrupulous list. fn do_remove_unscrupulous_items( out_accounts: &mut Vec, out_webs: &mut Vec>, ) -> DispatchResult { if !out_accounts.is_empty() { >::try_mutate(|accounts| -> DispatchResult { for who in out_accounts.iter() { let pos = accounts .binary_search(who) .ok() .ok_or(Error::::NotListedAsUnscrupulous)?; accounts.remove(pos); } Ok(()) })?; } if !out_webs.is_empty() { >::try_mutate(|webs| -> DispatchResult { for web in out_webs.iter() { let pos = webs .binary_search(web) .ok() .ok_or(Error::::NotListedAsUnscrupulous)?; webs.remove(pos); } Ok(()) })?; } Ok(()) } fn has_identity(who: &T::AccountId) -> DispatchResult { let judgement = |who: &T::AccountId| -> DispatchResult { ensure!( T::IdentityVerifier::has_required_identities(who), Error::::WithoutRequiredIdentityFields ); ensure!( T::IdentityVerifier::has_good_judgement(who), Error::::WithoutGoodIdentityJudgement ); Ok(()) }; let res = judgement(who); if res.is_err() { if let Some(parent) = T::IdentityVerifier::super_account_id(who) { return judgement(&parent); } } res } fn do_close( proposal_hash: T::Hash, index: ProposalIndex, proposal_weight_bound: Weight, length_bound: u32, ) -> DispatchResultWithPostInfo { let info = T::ProposalProvider::close_proposal( proposal_hash, index, proposal_weight_bound, length_bound, )?; Ok(info.into()) } } #[cfg(any(feature = "try-runtime", test))] impl, I: 'static> Pezpallet { /// Ensure the correctness of the state of this pezpallet. /// /// This should be valid before or after each state transition of this pezpallet. pub fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> { Self::try_state_members_are_disjoint()?; Self::try_state_members_are_sorted()?; Self::try_state_retiring_members_are_consistent()?; Self::try_state_deposit_of_is_consistent()?; Self::try_state_unscrupulous_items_are_sorted()?; Self::try_state_announcements_are_sorted()?; Ok(()) } /// # Invariants /// /// * The sets of `Fellows`, and `Allies` members must be mutually exclusive. An account cannot /// hold more than one role at a time. fn try_state_members_are_disjoint() -> Result<(), pezsp_runtime::TryRuntimeError> { let fellows = Members::::get(MemberRole::Fellow); let allies = Members::::get(MemberRole::Ally); for fellow in fellows.iter() { ensure!(allies.binary_search(fellow).is_err(), "Member is both Fellow and Ally"); } Ok(()) } /// # Invariants /// /// * The list of members for each role (`Fellow`, `Ally`, `Retiring`) must be sorted by /// `AccountId`. This is crucial for efficient lookups using binary search. fn try_state_members_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> { let roles = [MemberRole::Fellow, MemberRole::Ally, MemberRole::Retiring]; for role in roles.iter() { let members = Members::::get(role); let mut sorted_members = members.clone(); sorted_members.sort(); ensure!(members == sorted_members, "Members of a role are not sorted"); } Ok(()) } /// # Invariants /// /// * The set of accounts in `RetiringMembers` storage must be identical to the set of members /// with the `Retiring` role. fn try_state_retiring_members_are_consistent() -> Result<(), pezsp_runtime::TryRuntimeError> { let retiring_in_members = Members::::get(MemberRole::Retiring); let retiring_keys_count = RetiringMembers::::iter_keys().count(); ensure!( retiring_in_members.len() == retiring_keys_count, "Count mismatch between Members and RetiringMembers map" ); for member in retiring_in_members.iter() { ensure!( RetiringMembers::::contains_key(member), "Retiring member not found in RetiringMembers map" ); } Ok(()) } /// # Invariants /// /// * Every account that has a deposit stored in `DepositOf` must be a member of the alliance /// (either a `Fellow`, `Ally`, or `Retiring`). fn try_state_deposit_of_is_consistent() -> Result<(), pezsp_runtime::TryRuntimeError> { for (who, _) in DepositOf::::iter() { ensure!(Self::is_member(&who), "Account with deposit is not an alliance member"); } Ok(()) } /// # Invariants /// /// * The lists of `UnscrupulousAccounts` and `UnscrupulousWebsites` must be sorted. This allows /// for efficient binary search lookups. fn try_state_unscrupulous_items_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> { let accounts = UnscrupulousAccounts::::get(); let mut sorted_accounts = accounts.clone(); sorted_accounts.sort(); ensure!(accounts == sorted_accounts, "UnscrupulousAccounts is not sorted"); let websites = UnscrupulousWebsites::::get(); let mut sorted_websites = websites.clone(); sorted_websites.sort(); ensure!(websites == sorted_websites, "UnscrupulousWebsites is not sorted"); Ok(()) } /// # Invariants /// /// * The list of `Announcements` must be sorted. This is necessary because /// `remove_announcement` uses binary search. fn try_state_announcements_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> { let announcements = Announcements::::get(); let mut sorted_announcements = announcements.clone(); sorted_announcements.sort(); ensure!(announcements == sorted_announcements, "Announcements is not sorted"); Ok(()) } }