pezpallet_alliance/
lib.rs

1// This file is part of Bizinikiwi.
2
3// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Alliance Pezpallet
19//!
20//! The Alliance Pezpallet provides a collective that curates a list of accounts and URLs, deemed by
21//! the voting members to be unscrupulous actors. The Alliance
22//!
23//! - provides a set of ethics against bad behavior, and
24//! - provides recognition and influence for those teams that contribute something back to the
25//!   ecosystem.
26//!
27//! ## Overview
28//!
29//! The network initializes the Alliance via a Root call. After that, anyone with an approved
30//! identity and website can join as an Ally. The `MembershipManager` origin can elevate Allies to
31//! Fellows, giving them voting rights within the Alliance.
32//!
33//! Voting members of the Alliance maintain a list of accounts and websites. Members can also vote
34//! to update the Alliance's rule and make announcements.
35//!
36//! ### Terminology
37//!
38//! - Rule: The IPFS CID (hash) of the Alliance rules for the community to read and the Alliance
39//!   members to enforce. Similar to a Charter or Code of Conduct.
40//! - Announcement: An IPFS CID of some content that the Alliance want to announce.
41//! - Member: An account that is already in the group of the Alliance, including two types: Fellow,
42//!   or Ally. A member can also be kicked by the `MembershipManager` origin or retire by itself.
43//! - Fellow: An account who is elevated from Ally by other Fellows.
44//! - Ally: An account who would like to join the Alliance. To become a voting member (Fellow), it
45//!   will need approval from the `MembershipManager` origin. Any account can join as an Ally either
46//!   by placing a deposit or by nomination from a voting member.
47//! - Unscrupulous List: A list of bad websites and addresses; items can be added or removed by
48//!   voting members.
49//!
50//! ## Interface
51//!
52//! ### Dispatchable Functions
53//!
54//! #### For General Users
55//!
56//! - `join_alliance` - Join the Alliance as an Ally. This requires a slashable deposit.
57//!
58//! #### For Members (All)
59//!
60//! - `give_retirement_notice` - Give a retirement notice and start a retirement period required to
61//!   pass in order to retire.
62//! - `retire` - Retire from the Alliance and release the caller's deposit.
63//!
64//! #### For Voting Members
65//!
66//! - `propose` - Propose a motion.
67//! - `vote` - Vote on a motion.
68//! - `close` - Close a motion with enough votes or that has expired.
69//! - `set_rule` - Initialize or update the Alliance's rule by IPFS CID.
70//! - `announce` - Make announcement by IPFS CID.
71//! - `nominate_ally` - Nominate a non-member to become an Ally, without deposit.
72//! - `elevate_ally` - Approve an ally to become a Fellow.
73//! - `kick_member` - Kick a member and slash its deposit.
74//! - `add_unscrupulous_items` - Add some items, either accounts or websites, to the list of
75//!   unscrupulous items.
76//! - `remove_unscrupulous_items` - Remove some items from the list of unscrupulous items.
77//! - `abdicate_fellow_status` - Abdicate one's voting rights, demoting themself to Ally.
78//!
79//! #### Root Calls
80//!
81//! - `init_members` - Initialize the Alliance, onboard fellows and allies.
82//! - `disband` - Disband the Alliance, remove all active members and unreserve deposits.
83
84#![cfg_attr(not(feature = "std"), no_std)]
85
86#[cfg(test)]
87mod mock;
88#[cfg(test)]
89mod tests;
90
91#[cfg(feature = "runtime-benchmarks")]
92mod benchmarking;
93pub mod migration;
94mod types;
95pub mod weights;
96
97extern crate alloc;
98
99use alloc::{boxed::Box, vec, vec::Vec};
100use codec::{Decode, Encode, MaxEncodedLen};
101use pezframe_support::pezpallet_prelude::*;
102use pezframe_system::pezpallet_prelude::*;
103use pezsp_runtime::{
104	traits::{Dispatchable, Saturating, StaticLookup, Zero},
105	DispatchError, RuntimeDebug,
106};
107
108use pezframe_support::{
109	dispatch::{DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, PostDispatchInfo},
110	ensure,
111	traits::{
112		ChangeMembers, Currency, Get, InitializeMembers, IsSubType, OnUnbalanced,
113		ReservableCurrency,
114	},
115	weights::Weight,
116};
117use scale_info::TypeInfo;
118
119pub use pezpallet::*;
120pub use types::*;
121pub use weights::*;
122
123/// The log target of this pezpallet.
124pub const LOG_TARGET: &str = "runtime::alliance";
125
126/// Simple index type for proposal counting.
127pub type ProposalIndex = u32;
128
129type UrlOf<T, I> = BoundedVec<u8, <T as pezpallet::Config<I>>::MaxWebsiteUrlLength>;
130
131type BalanceOf<T, I> =
132	<<T as Config<I>>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
133type NegativeImbalanceOf<T, I> = <<T as Config<I>>::Currency as Currency<
134	<T as pezframe_system::Config>::AccountId,
135>>::NegativeImbalance;
136
137/// Interface required for identity verification.
138pub trait IdentityVerifier<AccountId> {
139	/// Function that returns whether an account has the required identities registered with the
140	/// identity provider.
141	fn has_required_identities(who: &AccountId) -> bool;
142
143	/// Whether an account has been deemed "good" by the provider.
144	fn has_good_judgement(who: &AccountId) -> bool;
145
146	/// If the identity provider allows sub-accounts, provide the super of an account. Should
147	/// return `None` if the provider does not allow sub-accounts or if the account is not a sub.
148	fn super_account_id(who: &AccountId) -> Option<AccountId>;
149}
150
151/// The non-provider. Imposes no restrictions on account identity.
152impl<AccountId> IdentityVerifier<AccountId> for () {
153	fn has_required_identities(_who: &AccountId) -> bool {
154		true
155	}
156
157	fn has_good_judgement(_who: &AccountId) -> bool {
158		true
159	}
160
161	fn super_account_id(_who: &AccountId) -> Option<AccountId> {
162		None
163	}
164}
165
166/// The provider of a collective action interface, for example an instance of
167/// `pezpallet-collective`.
168pub trait ProposalProvider<AccountId, Hash, Proposal> {
169	/// Add a new proposal.
170	/// Returns a proposal length and active proposals count if successful.
171	fn propose_proposal(
172		who: AccountId,
173		threshold: u32,
174		proposal: Box<Proposal>,
175		length_bound: u32,
176	) -> Result<(u32, u32), DispatchError>;
177
178	/// Add an aye or nay vote for the sender to the given proposal.
179	/// Returns true if the sender votes first time if successful.
180	fn vote_proposal(
181		who: AccountId,
182		proposal: Hash,
183		index: ProposalIndex,
184		approve: bool,
185	) -> Result<bool, DispatchError>;
186
187	/// Close a proposal that is either approved, disapproved, or whose voting period has ended.
188	fn close_proposal(
189		proposal_hash: Hash,
190		index: ProposalIndex,
191		proposal_weight_bound: Weight,
192		length_bound: u32,
193	) -> DispatchResultWithPostInfo;
194
195	/// Return a proposal of the given hash.
196	fn proposal_of(proposal_hash: Hash) -> Option<Proposal>;
197}
198
199/// The various roles that a member can hold.
200#[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]
201pub enum MemberRole {
202	Fellow,
203	Ally,
204	Retiring,
205}
206
207/// The type of item that may be deemed unscrupulous.
208#[derive(
209	Clone,
210	PartialEq,
211	Eq,
212	RuntimeDebug,
213	Encode,
214	Decode,
215	DecodeWithMemTracking,
216	TypeInfo,
217	MaxEncodedLen,
218)]
219pub enum UnscrupulousItem<AccountId, Url> {
220	AccountId(AccountId),
221	Website(Url),
222}
223
224type UnscrupulousItemOf<T, I> =
225	UnscrupulousItem<<T as pezframe_system::Config>::AccountId, UrlOf<T, I>>;
226
227type AccountIdLookupOf<T> = <<T as pezframe_system::Config>::Lookup as StaticLookup>::Source;
228
229#[pezframe_support::pezpallet]
230pub mod pezpallet {
231	use super::*;
232
233	#[pezpallet::pezpallet]
234	#[pezpallet::storage_version(migration::STORAGE_VERSION)]
235	pub struct Pezpallet<T, I = ()>(PhantomData<(T, I)>);
236
237	#[pezpallet::config]
238	pub trait Config<I: 'static = ()>: pezframe_system::Config {
239		/// The overarching event type.
240		#[allow(deprecated)]
241		type RuntimeEvent: From<Event<Self, I>>
242			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
243
244		/// The runtime call dispatch type.
245		type Proposal: Parameter
246			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
247			+ From<pezframe_system::Call<Self>>
248			+ From<Call<Self, I>>
249			+ GetDispatchInfo
250			+ IsSubType<Call<Self, I>>
251			+ IsType<<Self as pezframe_system::Config>::RuntimeCall>;
252
253		/// Origin for admin-level operations, like setting the Alliance's rules.
254		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
255
256		/// Origin that manages entry and forcible discharge from the Alliance.
257		type MembershipManager: EnsureOrigin<Self::RuntimeOrigin>;
258
259		/// Origin for making announcements and adding/removing unscrupulous items.
260		type AnnouncementOrigin: EnsureOrigin<Self::RuntimeOrigin>;
261
262		/// The currency used for deposits.
263		type Currency: ReservableCurrency<Self::AccountId>;
264
265		/// What to do with slashed funds.
266		type Slashed: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
267
268		/// What to do with initial voting members of the Alliance.
269		type InitializeMembers: InitializeMembers<Self::AccountId>;
270
271		/// What to do when a member has been added or removed.
272		type MembershipChanged: ChangeMembers<Self::AccountId>;
273
274		/// The identity verifier of an Alliance member.
275		type IdentityVerifier: IdentityVerifier<Self::AccountId>;
276
277		/// The provider of the proposal operation.
278		type ProposalProvider: ProposalProvider<Self::AccountId, Self::Hash, Self::Proposal>;
279
280		/// Maximum number of proposals allowed to be active in parallel.
281		type MaxProposals: Get<ProposalIndex>;
282
283		/// The maximum number of Fellows supported by the pezpallet. Used for weight estimation.
284		///
285		/// NOTE:
286		/// + Benchmarks will need to be re-run and weights adjusted if this changes.
287		/// + This pezpallet assumes that dependencies keep to the limit without enforcing it.
288		type MaxFellows: Get<u32>;
289
290		/// The maximum number of Allies supported by the pezpallet. Used for weight estimation.
291		///
292		/// NOTE:
293		/// + Benchmarks will need to be re-run and weights adjusted if this changes.
294		/// + This pezpallet assumes that dependencies keep to the limit without enforcing it.
295		type MaxAllies: Get<u32>;
296
297		/// The maximum number of the unscrupulous items supported by the pezpallet.
298		#[pezpallet::constant]
299		type MaxUnscrupulousItems: Get<u32>;
300
301		/// The maximum length of a website URL.
302		#[pezpallet::constant]
303		type MaxWebsiteUrlLength: Get<u32>;
304
305		/// The deposit required for submitting candidacy.
306		#[pezpallet::constant]
307		type AllyDeposit: Get<BalanceOf<Self, I>>;
308
309		/// The maximum number of announcements.
310		#[pezpallet::constant]
311		type MaxAnnouncementsCount: Get<u32>;
312
313		/// The maximum number of members per member role.
314		#[pezpallet::constant]
315		type MaxMembersCount: Get<u32>;
316
317		/// Weight information for extrinsics in this pezpallet.
318		type WeightInfo: WeightInfo;
319
320		/// The number of blocks a member must wait between giving a retirement notice and retiring.
321		/// Supposed to be greater than time required to `kick_member`.
322		type RetirementPeriod: Get<BlockNumberFor<Self>>;
323	}
324
325	#[pezpallet::error]
326	pub enum Error<T, I = ()> {
327		/// The Alliance has not been initialized yet, therefore accounts cannot join it.
328		AllianceNotYetInitialized,
329		/// The Alliance has been initialized, therefore cannot be initialized again.
330		AllianceAlreadyInitialized,
331		/// Account is already a member.
332		AlreadyMember,
333		/// Account is not a member.
334		NotMember,
335		/// Account is not an ally.
336		NotAlly,
337		/// Account does not have voting rights.
338		NoVotingRights,
339		/// Account is already an elevated (fellow) member.
340		AlreadyElevated,
341		/// Item is already listed as unscrupulous.
342		AlreadyUnscrupulous,
343		/// Account has been deemed unscrupulous by the Alliance and is not welcome to join or be
344		/// nominated.
345		AccountNonGrata,
346		/// Item has not been deemed unscrupulous.
347		NotListedAsUnscrupulous,
348		/// The number of unscrupulous items exceeds `MaxUnscrupulousItems`.
349		TooManyUnscrupulousItems,
350		/// Length of website URL exceeds `MaxWebsiteUrlLength`.
351		TooLongWebsiteUrl,
352		/// Balance is insufficient for the required deposit.
353		InsufficientFunds,
354		/// The account's identity does not have display field and website field.
355		WithoutRequiredIdentityFields,
356		/// The account's identity has no good judgement.
357		WithoutGoodIdentityJudgement,
358		/// The proposal hash is not found.
359		MissingProposalHash,
360		/// The announcement is not found.
361		MissingAnnouncement,
362		/// Number of members exceeds `MaxMembersCount`.
363		TooManyMembers,
364		/// Number of announcements exceeds `MaxAnnouncementsCount`.
365		TooManyAnnouncements,
366		/// Invalid witness data given.
367		BadWitness,
368		/// Account already gave retirement notice
369		AlreadyRetiring,
370		/// Account did not give a retirement notice required to retire.
371		RetirementNoticeNotGiven,
372		/// Retirement period has not passed.
373		RetirementPeriodNotPassed,
374		/// Fellows must be provided to initialize the Alliance.
375		FellowsMissing,
376	}
377
378	#[pezpallet::event]
379	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
380	pub enum Event<T: Config<I>, I: 'static = ()> {
381		/// A new rule has been set.
382		NewRuleSet { rule: Cid },
383		/// A new announcement has been proposed.
384		Announced { announcement: Cid },
385		/// An on-chain announcement has been removed.
386		AnnouncementRemoved { announcement: Cid },
387		/// Some accounts have been initialized as members (fellows/allies).
388		MembersInitialized { fellows: Vec<T::AccountId>, allies: Vec<T::AccountId> },
389		/// An account has been added as an Ally and reserved its deposit.
390		NewAllyJoined {
391			ally: T::AccountId,
392			nominator: Option<T::AccountId>,
393			reserved: Option<BalanceOf<T, I>>,
394		},
395		/// An ally has been elevated to Fellow.
396		AllyElevated { ally: T::AccountId },
397		/// A member gave retirement notice and their retirement period started.
398		MemberRetirementPeriodStarted { member: T::AccountId },
399		/// A member has retired with its deposit unreserved.
400		MemberRetired { member: T::AccountId, unreserved: Option<BalanceOf<T, I>> },
401		/// A member has been kicked out with its deposit slashed.
402		MemberKicked { member: T::AccountId, slashed: Option<BalanceOf<T, I>> },
403		/// Accounts or websites have been added into the list of unscrupulous items.
404		UnscrupulousItemAdded { items: Vec<UnscrupulousItemOf<T, I>> },
405		/// Accounts or websites have been removed from the list of unscrupulous items.
406		UnscrupulousItemRemoved { items: Vec<UnscrupulousItemOf<T, I>> },
407		/// Alliance disbanded. Includes number deleted members and unreserved deposits.
408		AllianceDisbanded { fellow_members: u32, ally_members: u32, unreserved: u32 },
409		/// A Fellow abdicated their voting rights. They are now an Ally.
410		FellowAbdicated { fellow: T::AccountId },
411	}
412
413	#[pezpallet::genesis_config]
414	#[derive(pezframe_support::DefaultNoBound)]
415	pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
416		pub fellows: Vec<T::AccountId>,
417		pub allies: Vec<T::AccountId>,
418		#[serde(skip)]
419		pub phantom: PhantomData<(T, I)>,
420	}
421
422	#[pezpallet::hooks]
423	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pezpallet<T, I> {
424		#[cfg(feature = "try-runtime")]
425		fn try_state(_n: BlockNumberFor<T>) -> Result<(), pezsp_runtime::TryRuntimeError> {
426			Self::do_try_state()
427		}
428	}
429
430	#[pezpallet::genesis_build]
431	impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
432		fn build(&self) {
433			for m in self.fellows.iter().chain(self.allies.iter()) {
434				assert!(
435					Pezpallet::<T, I>::has_identity(m).is_ok(),
436					"Member does not set identity!"
437				);
438			}
439
440			if !self.fellows.is_empty() {
441				assert!(
442					!Pezpallet::<T, I>::has_member(MemberRole::Fellow),
443					"Fellows are already initialized!"
444				);
445				let members: BoundedVec<T::AccountId, T::MaxMembersCount> =
446					self.fellows.clone().try_into().expect("Too many genesis fellows");
447				Members::<T, I>::insert(MemberRole::Fellow, members);
448			}
449			if !self.allies.is_empty() {
450				assert!(
451					!Pezpallet::<T, I>::has_member(MemberRole::Ally),
452					"Allies are already initialized!"
453				);
454				assert!(
455					!self.fellows.is_empty(),
456					"Fellows must be provided to initialize the Alliance"
457				);
458				let members: BoundedVec<T::AccountId, T::MaxMembersCount> =
459					self.allies.clone().try_into().expect("Too many genesis allies");
460				Members::<T, I>::insert(MemberRole::Ally, members);
461			}
462
463			T::InitializeMembers::initialize_members(self.fellows.as_slice())
464		}
465	}
466
467	/// The IPFS CID of the alliance rule.
468	/// Fellows can propose a new rule with a super-majority.
469	#[pezpallet::storage]
470	pub type Rule<T: Config<I>, I: 'static = ()> = StorageValue<_, Cid, OptionQuery>;
471
472	/// The current IPFS CIDs of any announcements.
473	#[pezpallet::storage]
474	pub type Announcements<T: Config<I>, I: 'static = ()> =
475		StorageValue<_, BoundedVec<Cid, T::MaxAnnouncementsCount>, ValueQuery>;
476
477	/// Maps members to their candidacy deposit.
478	#[pezpallet::storage]
479	pub type DepositOf<T: Config<I>, I: 'static = ()> =
480		StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T, I>, OptionQuery>;
481
482	/// Maps member type to members of each type.
483	#[pezpallet::storage]
484	pub type Members<T: Config<I>, I: 'static = ()> = StorageMap<
485		_,
486		Twox64Concat,
487		MemberRole,
488		BoundedVec<T::AccountId, T::MaxMembersCount>,
489		ValueQuery,
490	>;
491
492	/// A set of members who gave a retirement notice. They can retire after the end of retirement
493	/// period stored as a future block number.
494	#[pezpallet::storage]
495	pub type RetiringMembers<T: Config<I>, I: 'static = ()> =
496		StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor<T>, OptionQuery>;
497
498	/// The current list of accounts deemed unscrupulous. These accounts non grata cannot submit
499	/// candidacy.
500	#[pezpallet::storage]
501	pub type UnscrupulousAccounts<T: Config<I>, I: 'static = ()> =
502		StorageValue<_, BoundedVec<T::AccountId, T::MaxUnscrupulousItems>, ValueQuery>;
503
504	/// The current list of websites deemed unscrupulous.
505	#[pezpallet::storage]
506	pub type UnscrupulousWebsites<T: Config<I>, I: 'static = ()> =
507		StorageValue<_, BoundedVec<UrlOf<T, I>, T::MaxUnscrupulousItems>, ValueQuery>;
508
509	#[pezpallet::call(weight(<T as Config<I>>::WeightInfo))]
510	impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
511		/// Add a new proposal to be voted on.
512		///
513		/// Must be called by a Fellow.
514		#[pezpallet::call_index(0)]
515		#[pezpallet::weight(T::WeightInfo::propose_proposed(
516			*length_bound, // B
517			T::MaxFellows::get(), // M
518			T::MaxProposals::get(), // P2
519		))]
520		pub fn propose(
521			origin: OriginFor<T>,
522			#[pezpallet::compact] threshold: u32,
523			proposal: Box<<T as Config<I>>::Proposal>,
524			#[pezpallet::compact] length_bound: u32,
525		) -> DispatchResult {
526			let proposer = ensure_signed(origin)?;
527			ensure!(Self::has_voting_rights(&proposer), Error::<T, I>::NoVotingRights);
528
529			T::ProposalProvider::propose_proposal(proposer, threshold, proposal, length_bound)?;
530			Ok(())
531		}
532
533		/// Add an aye or nay vote for the sender to the given proposal.
534		///
535		/// Must be called by a Fellow.
536		#[pezpallet::call_index(1)]
537		#[pezpallet::weight(T::WeightInfo::vote(T::MaxFellows::get()))]
538		pub fn vote(
539			origin: OriginFor<T>,
540			proposal: T::Hash,
541			#[pezpallet::compact] index: ProposalIndex,
542			approve: bool,
543		) -> DispatchResult {
544			let who = ensure_signed(origin)?;
545			ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
546
547			T::ProposalProvider::vote_proposal(who, proposal, index, approve)?;
548			Ok(())
549		}
550
551		// Index 2 was `close_old_weight`; it was removed due to weights v1 deprecation.
552
553		/// Initialize the Alliance, onboard fellows and allies.
554		///
555		/// The Alliance must be empty, and the call must provide some founding members.
556		///
557		/// Must be called by the Root origin.
558		#[pezpallet::call_index(3)]
559		#[pezpallet::weight(T::WeightInfo::init_members(
560			fellows.len() as u32,
561			allies.len() as u32,
562		))]
563		pub fn init_members(
564			origin: OriginFor<T>,
565			fellows: Vec<T::AccountId>,
566			allies: Vec<T::AccountId>,
567		) -> DispatchResult {
568			ensure_root(origin)?;
569
570			ensure!(!fellows.is_empty(), Error::<T, I>::FellowsMissing);
571			ensure!(!Self::is_initialized(), Error::<T, I>::AllianceAlreadyInitialized);
572
573			let mut fellows: BoundedVec<T::AccountId, T::MaxMembersCount> =
574				fellows.try_into().map_err(|_| Error::<T, I>::TooManyMembers)?;
575			let mut allies: BoundedVec<T::AccountId, T::MaxMembersCount> =
576				allies.try_into().map_err(|_| Error::<T, I>::TooManyMembers)?;
577
578			for member in fellows.iter().chain(allies.iter()) {
579				Self::has_identity(member)?;
580			}
581
582			fellows.sort();
583			Members::<T, I>::insert(&MemberRole::Fellow, fellows.clone());
584			allies.sort();
585			Members::<T, I>::insert(&MemberRole::Ally, allies.clone());
586
587			let mut voteable_members = fellows.clone();
588			voteable_members.sort();
589
590			T::InitializeMembers::initialize_members(&voteable_members);
591
592			log::debug!(
593				target: LOG_TARGET,
594				"Initialize alliance fellows: {:?}, allies: {:?}",
595				fellows,
596				allies
597			);
598
599			Self::deposit_event(Event::MembersInitialized {
600				fellows: fellows.into(),
601				allies: allies.into(),
602			});
603			Ok(())
604		}
605
606		/// Disband the Alliance, remove all active members and unreserve deposits.
607		///
608		/// Witness data must be set.
609		#[pezpallet::call_index(4)]
610		#[pezpallet::weight(T::WeightInfo::disband(
611			witness.fellow_members,
612			witness.ally_members,
613			witness.fellow_members.saturating_add(witness.ally_members),
614		))]
615		pub fn disband(
616			origin: OriginFor<T>,
617			witness: DisbandWitness,
618		) -> DispatchResultWithPostInfo {
619			ensure_root(origin)?;
620
621			ensure!(!witness.is_zero(), Error::<T, I>::BadWitness);
622			ensure!(
623				Self::voting_members_count() <= witness.fellow_members,
624				Error::<T, I>::BadWitness
625			);
626			ensure!(Self::ally_members_count() <= witness.ally_members, Error::<T, I>::BadWitness);
627			ensure!(Self::is_initialized(), Error::<T, I>::AllianceNotYetInitialized);
628
629			let voting_members = Self::voting_members();
630			T::MembershipChanged::change_members_sorted(&[], &voting_members, &[]);
631
632			let ally_members = Self::members_of(MemberRole::Ally);
633			let mut unreserve_count: u32 = 0;
634			for member in voting_members.iter().chain(ally_members.iter()) {
635				if let Some(deposit) = DepositOf::<T, I>::take(&member) {
636					let err_amount = T::Currency::unreserve(&member, deposit);
637					debug_assert!(err_amount.is_zero());
638					unreserve_count += 1;
639				}
640			}
641
642			Members::<T, I>::remove(&MemberRole::Fellow);
643			Members::<T, I>::remove(&MemberRole::Ally);
644
645			Self::deposit_event(Event::AllianceDisbanded {
646				fellow_members: voting_members.len() as u32,
647				ally_members: ally_members.len() as u32,
648				unreserved: unreserve_count,
649			});
650
651			Ok(Some(T::WeightInfo::disband(
652				voting_members.len() as u32,
653				ally_members.len() as u32,
654				unreserve_count,
655			))
656			.into())
657		}
658
659		/// Set a new IPFS CID to the alliance rule.
660		#[pezpallet::call_index(5)]
661		pub fn set_rule(origin: OriginFor<T>, rule: Cid) -> DispatchResult {
662			T::AdminOrigin::ensure_origin(origin)?;
663
664			Rule::<T, I>::put(&rule);
665
666			Self::deposit_event(Event::NewRuleSet { rule });
667			Ok(())
668		}
669
670		/// Make an announcement of a new IPFS CID about alliance issues.
671		#[pezpallet::call_index(6)]
672		pub fn announce(origin: OriginFor<T>, announcement: Cid) -> DispatchResult {
673			T::AnnouncementOrigin::ensure_origin(origin)?;
674
675			let mut announcements = <Announcements<T, I>>::get();
676			announcements
677				.try_push(announcement.clone())
678				.map_err(|_| Error::<T, I>::TooManyAnnouncements)?;
679			<Announcements<T, I>>::put(announcements);
680
681			Self::deposit_event(Event::Announced { announcement });
682			Ok(())
683		}
684
685		/// Remove an announcement.
686		#[pezpallet::call_index(7)]
687		pub fn remove_announcement(origin: OriginFor<T>, announcement: Cid) -> DispatchResult {
688			T::AnnouncementOrigin::ensure_origin(origin)?;
689
690			let mut announcements = <Announcements<T, I>>::get();
691			let pos = announcements
692				.binary_search(&announcement)
693				.ok()
694				.ok_or(Error::<T, I>::MissingAnnouncement)?;
695			announcements.remove(pos);
696			<Announcements<T, I>>::put(announcements);
697
698			Self::deposit_event(Event::AnnouncementRemoved { announcement });
699			Ok(())
700		}
701
702		/// Submit oneself for candidacy. A fixed deposit is reserved.
703		#[pezpallet::call_index(8)]
704		pub fn join_alliance(origin: OriginFor<T>) -> DispatchResult {
705			let who = ensure_signed(origin)?;
706
707			// We don't want anyone to join as an Ally before the Alliance has been initialized via
708			// Root call. The reasons are two-fold:
709			//
710			// 1. There is no `Rule` or admission criteria, so the joiner would be an ally to
711			//    nought, and
712			// 2. It adds complexity to the initialization, namely deciding to overwrite accounts
713			//    that already joined as an Ally.
714			ensure!(Self::is_initialized(), Error::<T, I>::AllianceNotYetInitialized);
715
716			// Unscrupulous accounts are non grata.
717			ensure!(!Self::is_unscrupulous_account(&who), Error::<T, I>::AccountNonGrata);
718			ensure!(!Self::is_member(&who), Error::<T, I>::AlreadyMember);
719			// check user self or parent should has verified identity to reuse display name and
720			// website.
721			Self::has_identity(&who)?;
722
723			let deposit = T::AllyDeposit::get();
724			T::Currency::reserve(&who, deposit).map_err(|_| Error::<T, I>::InsufficientFunds)?;
725			<DepositOf<T, I>>::insert(&who, deposit);
726
727			Self::add_member(&who, MemberRole::Ally)?;
728
729			Self::deposit_event(Event::NewAllyJoined {
730				ally: who,
731				nominator: None,
732				reserved: Some(deposit),
733			});
734			Ok(())
735		}
736
737		/// A Fellow can nominate someone to join the alliance as an Ally. There is no deposit
738		/// required from the nominator or nominee.
739		#[pezpallet::call_index(9)]
740		pub fn nominate_ally(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
741			let nominator = ensure_signed(origin)?;
742			ensure!(Self::has_voting_rights(&nominator), Error::<T, I>::NoVotingRights);
743			let who = T::Lookup::lookup(who)?;
744
745			// Individual voting members cannot nominate accounts non grata.
746			ensure!(!Self::is_unscrupulous_account(&who), Error::<T, I>::AccountNonGrata);
747			ensure!(!Self::is_member(&who), Error::<T, I>::AlreadyMember);
748			// check user self or parent should has verified identity to reuse display name and
749			// website.
750			Self::has_identity(&who)?;
751
752			Self::add_member(&who, MemberRole::Ally)?;
753
754			Self::deposit_event(Event::NewAllyJoined {
755				ally: who,
756				nominator: Some(nominator),
757				reserved: None,
758			});
759			Ok(())
760		}
761
762		/// Elevate an Ally to Fellow.
763		#[pezpallet::call_index(10)]
764		pub fn elevate_ally(origin: OriginFor<T>, ally: AccountIdLookupOf<T>) -> DispatchResult {
765			T::MembershipManager::ensure_origin(origin)?;
766			let ally = T::Lookup::lookup(ally)?;
767			ensure!(Self::is_ally(&ally), Error::<T, I>::NotAlly);
768			ensure!(!Self::has_voting_rights(&ally), Error::<T, I>::AlreadyElevated);
769
770			Self::remove_member(&ally, MemberRole::Ally)?;
771			Self::add_member(&ally, MemberRole::Fellow)?;
772
773			Self::deposit_event(Event::AllyElevated { ally });
774			Ok(())
775		}
776
777		/// As a member, give a retirement notice and start a retirement period required to pass in
778		/// order to retire.
779		#[pezpallet::call_index(11)]
780		pub fn give_retirement_notice(origin: OriginFor<T>) -> DispatchResult {
781			let who = ensure_signed(origin)?;
782			let role = Self::member_role_of(&who).ok_or(Error::<T, I>::NotMember)?;
783			ensure!(role.ne(&MemberRole::Retiring), Error::<T, I>::AlreadyRetiring);
784
785			Self::remove_member(&who, role)?;
786			Self::add_member(&who, MemberRole::Retiring)?;
787			<RetiringMembers<T, I>>::insert(
788				&who,
789				pezframe_system::Pezpallet::<T>::block_number()
790					.saturating_add(T::RetirementPeriod::get()),
791			);
792
793			Self::deposit_event(Event::MemberRetirementPeriodStarted { member: who });
794			Ok(())
795		}
796
797		/// As a member, retire from the Alliance and unreserve the deposit.
798		///
799		/// This can only be done once you have called `give_retirement_notice` and the
800		/// `RetirementPeriod` has passed.
801		#[pezpallet::call_index(12)]
802		pub fn retire(origin: OriginFor<T>) -> DispatchResult {
803			let who = ensure_signed(origin)?;
804			let retirement_period_end = RetiringMembers::<T, I>::get(&who)
805				.ok_or(Error::<T, I>::RetirementNoticeNotGiven)?;
806			ensure!(
807				pezframe_system::Pezpallet::<T>::block_number() >= retirement_period_end,
808				Error::<T, I>::RetirementPeriodNotPassed
809			);
810
811			Self::remove_member(&who, MemberRole::Retiring)?;
812			<RetiringMembers<T, I>>::remove(&who);
813			let deposit = DepositOf::<T, I>::take(&who);
814			if let Some(deposit) = deposit {
815				let err_amount = T::Currency::unreserve(&who, deposit);
816				debug_assert!(err_amount.is_zero());
817			}
818			Self::deposit_event(Event::MemberRetired { member: who, unreserved: deposit });
819			Ok(())
820		}
821
822		/// Kick a member from the Alliance and slash its deposit.
823		#[pezpallet::call_index(13)]
824		pub fn kick_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
825			T::MembershipManager::ensure_origin(origin)?;
826			let member = T::Lookup::lookup(who)?;
827
828			let role = Self::member_role_of(&member).ok_or(Error::<T, I>::NotMember)?;
829			Self::remove_member(&member, role)?;
830			let deposit = DepositOf::<T, I>::take(member.clone());
831			if let Some(deposit) = deposit {
832				T::Slashed::on_unbalanced(T::Currency::slash_reserved(&member, deposit).0);
833			}
834
835			Self::deposit_event(Event::MemberKicked { member, slashed: deposit });
836			Ok(())
837		}
838
839		/// Add accounts or websites to the list of unscrupulous items.
840		#[pezpallet::call_index(14)]
841		#[pezpallet::weight(T::WeightInfo::add_unscrupulous_items(items.len() as u32, T::MaxWebsiteUrlLength::get()))]
842		pub fn add_unscrupulous_items(
843			origin: OriginFor<T>,
844			items: Vec<UnscrupulousItemOf<T, I>>,
845		) -> DispatchResult {
846			T::AnnouncementOrigin::ensure_origin(origin)?;
847
848			let mut accounts = vec![];
849			let mut webs = vec![];
850			for info in items.iter() {
851				ensure!(!Self::is_unscrupulous(info), Error::<T, I>::AlreadyUnscrupulous);
852				match info {
853					UnscrupulousItem::AccountId(who) => accounts.push(who.clone()),
854					UnscrupulousItem::Website(url) => {
855						ensure!(
856							url.len() as u32 <= T::MaxWebsiteUrlLength::get(),
857							Error::<T, I>::TooLongWebsiteUrl
858						);
859						webs.push(url.clone());
860					},
861				}
862			}
863
864			Self::do_add_unscrupulous_items(&mut accounts, &mut webs)?;
865			Self::deposit_event(Event::UnscrupulousItemAdded { items });
866			Ok(())
867		}
868
869		/// Deem some items no longer unscrupulous.
870		#[pezpallet::call_index(15)]
871		#[pezpallet::weight(<T as Config<I>>::WeightInfo::remove_unscrupulous_items(
872			items.len() as u32, T::MaxWebsiteUrlLength::get()
873		))]
874		pub fn remove_unscrupulous_items(
875			origin: OriginFor<T>,
876			items: Vec<UnscrupulousItemOf<T, I>>,
877		) -> DispatchResult {
878			T::AnnouncementOrigin::ensure_origin(origin)?;
879			let mut accounts = vec![];
880			let mut webs = vec![];
881			for info in items.iter() {
882				ensure!(Self::is_unscrupulous(info), Error::<T, I>::NotListedAsUnscrupulous);
883				match info {
884					UnscrupulousItem::AccountId(who) => accounts.push(who.clone()),
885					UnscrupulousItem::Website(url) => webs.push(url.clone()),
886				}
887			}
888			Self::do_remove_unscrupulous_items(&mut accounts, &mut webs)?;
889			Self::deposit_event(Event::UnscrupulousItemRemoved { items });
890			Ok(())
891		}
892
893		/// Close a vote that is either approved, disapproved, or whose voting period has ended.
894		///
895		/// Must be called by a Fellow.
896		#[pezpallet::call_index(16)]
897		#[pezpallet::weight({
898			let b = *length_bound;
899			let m = T::MaxFellows::get();
900			let p1 = *proposal_weight_bound;
901			let p2 = T::MaxProposals::get();
902			T::WeightInfo::close_early_approved(b, m, p2)
903				.max(T::WeightInfo::close_early_disapproved(m, p2))
904				.max(T::WeightInfo::close_approved(b, m, p2))
905				.max(T::WeightInfo::close_disapproved(m, p2))
906				.saturating_add(p1)
907		})]
908		pub fn close(
909			origin: OriginFor<T>,
910			proposal_hash: T::Hash,
911			#[pezpallet::compact] index: ProposalIndex,
912			proposal_weight_bound: Weight,
913			#[pezpallet::compact] length_bound: u32,
914		) -> DispatchResultWithPostInfo {
915			let who = ensure_signed(origin)?;
916			ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
917
918			Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
919		}
920
921		/// Abdicate one's position as a voting member and just be an Ally. May be used by Fellows
922		/// who do not want to leave the Alliance but do not have the capacity to participate
923		/// operationally for some time.
924		#[pezpallet::call_index(17)]
925		pub fn abdicate_fellow_status(origin: OriginFor<T>) -> DispatchResult {
926			let who = ensure_signed(origin)?;
927			let role = Self::member_role_of(&who).ok_or(Error::<T, I>::NotMember)?;
928			// Not applicable to members who are retiring or who are already Allies.
929			ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
930
931			Self::remove_member(&who, role)?;
932			Self::add_member(&who, MemberRole::Ally)?;
933
934			Self::deposit_event(Event::FellowAbdicated { fellow: who });
935			Ok(())
936		}
937	}
938}
939
940impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
941	/// Check if the Alliance has been initialized.
942	fn is_initialized() -> bool {
943		Self::has_member(MemberRole::Fellow) || Self::has_member(MemberRole::Ally)
944	}
945
946	/// Check if a given role has any members.
947	fn has_member(role: MemberRole) -> bool {
948		Members::<T, I>::decode_len(role).unwrap_or_default() > 0
949	}
950
951	/// Look up the role, if any, of an account.
952	fn member_role_of(who: &T::AccountId) -> Option<MemberRole> {
953		Members::<T, I>::iter()
954			.find_map(|(r, members)| if members.contains(who) { Some(r) } else { None })
955	}
956
957	/// Check if a user is a alliance member.
958	pub fn is_member(who: &T::AccountId) -> bool {
959		Self::member_role_of(who).is_some()
960	}
961
962	/// Check if an account has a given role.
963	pub fn is_member_of(who: &T::AccountId, role: MemberRole) -> bool {
964		Members::<T, I>::get(role).contains(&who)
965	}
966
967	/// Check if an account is an Ally.
968	fn is_ally(who: &T::AccountId) -> bool {
969		Self::is_member_of(who, MemberRole::Ally)
970	}
971
972	/// Check if a member has voting rights.
973	fn has_voting_rights(who: &T::AccountId) -> bool {
974		Self::is_member_of(who, MemberRole::Fellow)
975	}
976
977	/// Count of ally members.
978	fn ally_members_count() -> u32 {
979		Members::<T, I>::decode_len(MemberRole::Ally).unwrap_or(0) as u32
980	}
981
982	/// Count of all members who have voting rights.
983	fn voting_members_count() -> u32 {
984		Members::<T, I>::decode_len(MemberRole::Fellow).unwrap_or(0) as u32
985	}
986
987	/// Get all members of a given role.
988	fn members_of(role: MemberRole) -> Vec<T::AccountId> {
989		Members::<T, I>::get(role).into_inner()
990	}
991
992	/// Collect all members who have voting rights into one list.
993	fn voting_members() -> Vec<T::AccountId> {
994		Self::members_of(MemberRole::Fellow)
995	}
996
997	/// Add a user to the sorted alliance member set.
998	fn add_member(who: &T::AccountId, role: MemberRole) -> DispatchResult {
999		<Members<T, I>>::try_mutate(role, |members| -> DispatchResult {
1000			let pos = members.binary_search(who).err().ok_or(Error::<T, I>::AlreadyMember)?;
1001			members
1002				.try_insert(pos, who.clone())
1003				.map_err(|_| Error::<T, I>::TooManyMembers)?;
1004			Ok(())
1005		})?;
1006
1007		if role == MemberRole::Fellow {
1008			let members = Self::voting_members();
1009			T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members[..]);
1010		}
1011		Ok(())
1012	}
1013
1014	/// Remove a user from the alliance member set.
1015	fn remove_member(who: &T::AccountId, role: MemberRole) -> DispatchResult {
1016		<Members<T, I>>::try_mutate(role, |members| -> DispatchResult {
1017			let pos = members.binary_search(who).ok().ok_or(Error::<T, I>::NotMember)?;
1018			members.remove(pos);
1019			Ok(())
1020		})?;
1021
1022		if role == MemberRole::Fellow {
1023			let members = Self::voting_members();
1024			T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]);
1025		}
1026		Ok(())
1027	}
1028
1029	/// Check if an item is listed as unscrupulous.
1030	fn is_unscrupulous(info: &UnscrupulousItemOf<T, I>) -> bool {
1031		match info {
1032			UnscrupulousItem::Website(url) => <UnscrupulousWebsites<T, I>>::get().contains(url),
1033			UnscrupulousItem::AccountId(who) => <UnscrupulousAccounts<T, I>>::get().contains(who),
1034		}
1035	}
1036
1037	/// Check if an account is listed as unscrupulous.
1038	fn is_unscrupulous_account(who: &T::AccountId) -> bool {
1039		<UnscrupulousAccounts<T, I>>::get().contains(who)
1040	}
1041
1042	/// Add item to the unscrupulous list.
1043	fn do_add_unscrupulous_items(
1044		new_accounts: &mut Vec<T::AccountId>,
1045		new_webs: &mut Vec<UrlOf<T, I>>,
1046	) -> DispatchResult {
1047		if !new_accounts.is_empty() {
1048			<UnscrupulousAccounts<T, I>>::try_mutate(|accounts| -> DispatchResult {
1049				accounts
1050					.try_append(new_accounts)
1051					.map_err(|_| Error::<T, I>::TooManyUnscrupulousItems)?;
1052				accounts.sort();
1053
1054				Ok(())
1055			})?;
1056		}
1057		if !new_webs.is_empty() {
1058			<UnscrupulousWebsites<T, I>>::try_mutate(|webs| -> DispatchResult {
1059				webs.try_append(new_webs).map_err(|_| Error::<T, I>::TooManyUnscrupulousItems)?;
1060				webs.sort();
1061
1062				Ok(())
1063			})?;
1064		}
1065
1066		Ok(())
1067	}
1068
1069	/// Remove item from the unscrupulous list.
1070	fn do_remove_unscrupulous_items(
1071		out_accounts: &mut Vec<T::AccountId>,
1072		out_webs: &mut Vec<UrlOf<T, I>>,
1073	) -> DispatchResult {
1074		if !out_accounts.is_empty() {
1075			<UnscrupulousAccounts<T, I>>::try_mutate(|accounts| -> DispatchResult {
1076				for who in out_accounts.iter() {
1077					let pos = accounts
1078						.binary_search(who)
1079						.ok()
1080						.ok_or(Error::<T, I>::NotListedAsUnscrupulous)?;
1081					accounts.remove(pos);
1082				}
1083				Ok(())
1084			})?;
1085		}
1086		if !out_webs.is_empty() {
1087			<UnscrupulousWebsites<T, I>>::try_mutate(|webs| -> DispatchResult {
1088				for web in out_webs.iter() {
1089					let pos = webs
1090						.binary_search(web)
1091						.ok()
1092						.ok_or(Error::<T, I>::NotListedAsUnscrupulous)?;
1093					webs.remove(pos);
1094				}
1095				Ok(())
1096			})?;
1097		}
1098		Ok(())
1099	}
1100
1101	fn has_identity(who: &T::AccountId) -> DispatchResult {
1102		let judgement = |who: &T::AccountId| -> DispatchResult {
1103			ensure!(
1104				T::IdentityVerifier::has_required_identities(who),
1105				Error::<T, I>::WithoutRequiredIdentityFields
1106			);
1107			ensure!(
1108				T::IdentityVerifier::has_good_judgement(who),
1109				Error::<T, I>::WithoutGoodIdentityJudgement
1110			);
1111			Ok(())
1112		};
1113
1114		let res = judgement(who);
1115		if res.is_err() {
1116			if let Some(parent) = T::IdentityVerifier::super_account_id(who) {
1117				return judgement(&parent);
1118			}
1119		}
1120		res
1121	}
1122
1123	fn do_close(
1124		proposal_hash: T::Hash,
1125		index: ProposalIndex,
1126		proposal_weight_bound: Weight,
1127		length_bound: u32,
1128	) -> DispatchResultWithPostInfo {
1129		let info = T::ProposalProvider::close_proposal(
1130			proposal_hash,
1131			index,
1132			proposal_weight_bound,
1133			length_bound,
1134		)?;
1135		Ok(info.into())
1136	}
1137}
1138
1139#[cfg(any(feature = "try-runtime", test))]
1140impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
1141	/// Ensure the correctness of the state of this pezpallet.
1142	///
1143	/// This should be valid before or after each state transition of this pezpallet.
1144	pub fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> {
1145		Self::try_state_members_are_disjoint()?;
1146		Self::try_state_members_are_sorted()?;
1147		Self::try_state_retiring_members_are_consistent()?;
1148		Self::try_state_deposit_of_is_consistent()?;
1149		Self::try_state_unscrupulous_items_are_sorted()?;
1150		Self::try_state_announcements_are_sorted()?;
1151		Ok(())
1152	}
1153
1154	/// # Invariants
1155	///
1156	/// * The sets of `Fellows`, and `Allies` members must be mutually exclusive. An account cannot
1157	///   hold more than one role at a time.
1158	fn try_state_members_are_disjoint() -> Result<(), pezsp_runtime::TryRuntimeError> {
1159		let fellows = Members::<T, I>::get(MemberRole::Fellow);
1160		let allies = Members::<T, I>::get(MemberRole::Ally);
1161
1162		for fellow in fellows.iter() {
1163			ensure!(allies.binary_search(fellow).is_err(), "Member is both Fellow and Ally");
1164		}
1165
1166		Ok(())
1167	}
1168
1169	/// # Invariants
1170	///
1171	/// * The list of members for each role (`Fellow`, `Ally`, `Retiring`) must be sorted by
1172	///   `AccountId`. This is crucial for efficient lookups using binary search.
1173	fn try_state_members_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> {
1174		let roles = [MemberRole::Fellow, MemberRole::Ally, MemberRole::Retiring];
1175		for role in roles.iter() {
1176			let members = Members::<T, I>::get(role);
1177			let mut sorted_members = members.clone();
1178			sorted_members.sort();
1179			ensure!(members == sorted_members, "Members of a role are not sorted");
1180		}
1181		Ok(())
1182	}
1183
1184	/// # Invariants
1185	///
1186	/// * The set of accounts in `RetiringMembers` storage must be identical to the set of members
1187	///   with the `Retiring` role.
1188	fn try_state_retiring_members_are_consistent() -> Result<(), pezsp_runtime::TryRuntimeError> {
1189		let retiring_in_members = Members::<T, I>::get(MemberRole::Retiring);
1190		let retiring_keys_count = RetiringMembers::<T, I>::iter_keys().count();
1191
1192		ensure!(
1193			retiring_in_members.len() == retiring_keys_count,
1194			"Count mismatch between Members<Retiring> and RetiringMembers map"
1195		);
1196
1197		for member in retiring_in_members.iter() {
1198			ensure!(
1199				RetiringMembers::<T, I>::contains_key(member),
1200				"Retiring member not found in RetiringMembers map"
1201			);
1202		}
1203
1204		Ok(())
1205	}
1206
1207	/// # Invariants
1208	///
1209	/// * Every account that has a deposit stored in `DepositOf` must be a member of the alliance
1210	///   (either a `Fellow`, `Ally`, or `Retiring`).
1211	fn try_state_deposit_of_is_consistent() -> Result<(), pezsp_runtime::TryRuntimeError> {
1212		for (who, _) in DepositOf::<T, I>::iter() {
1213			ensure!(Self::is_member(&who), "Account with deposit is not an alliance member");
1214		}
1215		Ok(())
1216	}
1217
1218	/// # Invariants
1219	///
1220	/// * The lists of `UnscrupulousAccounts` and `UnscrupulousWebsites` must be sorted. This allows
1221	///   for efficient binary search lookups.
1222	fn try_state_unscrupulous_items_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> {
1223		let accounts = UnscrupulousAccounts::<T, I>::get();
1224		let mut sorted_accounts = accounts.clone();
1225		sorted_accounts.sort();
1226		ensure!(accounts == sorted_accounts, "UnscrupulousAccounts is not sorted");
1227
1228		let websites = UnscrupulousWebsites::<T, I>::get();
1229		let mut sorted_websites = websites.clone();
1230		sorted_websites.sort();
1231		ensure!(websites == sorted_websites, "UnscrupulousWebsites is not sorted");
1232
1233		Ok(())
1234	}
1235
1236	/// # Invariants
1237	///
1238	/// * The list of `Announcements` must be sorted. This is necessary because
1239	///   `remove_announcement` uses binary search.
1240	fn try_state_announcements_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> {
1241		let announcements = Announcements::<T, I>::get();
1242		let mut sorted_announcements = announcements.clone();
1243		sorted_announcements.sort();
1244		ensure!(announcements == sorted_announcements, "Announcements is not sorted");
1245		Ok(())
1246	}
1247}