referrerpolicy=no-referrer-when-downgrade

pallet_alliance/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
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 Pallet
19//!
20//! The Alliance Pallet 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 frame_support::pallet_prelude::*;
102use frame_system::pallet_prelude::*;
103use sp_runtime::{
104	traits::{Dispatchable, Saturating, StaticLookup, Zero},
105	DispatchError, RuntimeDebug,
106};
107
108use frame_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 pallet::*;
120pub use types::*;
121pub use weights::*;
122
123/// The log target of this pallet.
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 pallet::Config<I>>::MaxWebsiteUrlLength>;
130
131type BalanceOf<T, I> =
132	<<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
133type NegativeImbalanceOf<T, I> = <<T as Config<I>>::Currency as Currency<
134	<T as frame_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 `pallet-collective`.
167pub trait ProposalProvider<AccountId, Hash, Proposal> {
168	/// Add a new proposal.
169	/// Returns a proposal length and active proposals count if successful.
170	fn propose_proposal(
171		who: AccountId,
172		threshold: u32,
173		proposal: Box<Proposal>,
174		length_bound: u32,
175	) -> Result<(u32, u32), DispatchError>;
176
177	/// Add an aye or nay vote for the sender to the given proposal.
178	/// Returns true if the sender votes first time if successful.
179	fn vote_proposal(
180		who: AccountId,
181		proposal: Hash,
182		index: ProposalIndex,
183		approve: bool,
184	) -> Result<bool, DispatchError>;
185
186	/// Close a proposal that is either approved, disapproved, or whose voting period has ended.
187	fn close_proposal(
188		proposal_hash: Hash,
189		index: ProposalIndex,
190		proposal_weight_bound: Weight,
191		length_bound: u32,
192	) -> DispatchResultWithPostInfo;
193
194	/// Return a proposal of the given hash.
195	fn proposal_of(proposal_hash: Hash) -> Option<Proposal>;
196}
197
198/// The various roles that a member can hold.
199#[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]
200pub enum MemberRole {
201	Fellow,
202	Ally,
203	Retiring,
204}
205
206/// The type of item that may be deemed unscrupulous.
207#[derive(
208	Clone,
209	PartialEq,
210	Eq,
211	RuntimeDebug,
212	Encode,
213	Decode,
214	DecodeWithMemTracking,
215	TypeInfo,
216	MaxEncodedLen,
217)]
218pub enum UnscrupulousItem<AccountId, Url> {
219	AccountId(AccountId),
220	Website(Url),
221}
222
223type UnscrupulousItemOf<T, I> =
224	UnscrupulousItem<<T as frame_system::Config>::AccountId, UrlOf<T, I>>;
225
226type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
227
228#[frame_support::pallet]
229pub mod pallet {
230	use super::*;
231
232	#[pallet::pallet]
233	#[pallet::storage_version(migration::STORAGE_VERSION)]
234	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
235
236	#[pallet::config]
237	pub trait Config<I: 'static = ()>: frame_system::Config {
238		/// The overarching event type.
239		#[allow(deprecated)]
240		type RuntimeEvent: From<Event<Self, I>>
241			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
242
243		/// The runtime call dispatch type.
244		type Proposal: Parameter
245			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
246			+ From<frame_system::Call<Self>>
247			+ From<Call<Self, I>>
248			+ GetDispatchInfo
249			+ IsSubType<Call<Self, I>>
250			+ IsType<<Self as frame_system::Config>::RuntimeCall>;
251
252		/// Origin for admin-level operations, like setting the Alliance's rules.
253		type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
254
255		/// Origin that manages entry and forcible discharge from the Alliance.
256		type MembershipManager: EnsureOrigin<Self::RuntimeOrigin>;
257
258		/// Origin for making announcements and adding/removing unscrupulous items.
259		type AnnouncementOrigin: EnsureOrigin<Self::RuntimeOrigin>;
260
261		/// The currency used for deposits.
262		type Currency: ReservableCurrency<Self::AccountId>;
263
264		/// What to do with slashed funds.
265		type Slashed: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
266
267		/// What to do with initial voting members of the Alliance.
268		type InitializeMembers: InitializeMembers<Self::AccountId>;
269
270		/// What to do when a member has been added or removed.
271		type MembershipChanged: ChangeMembers<Self::AccountId>;
272
273		/// The identity verifier of an Alliance member.
274		type IdentityVerifier: IdentityVerifier<Self::AccountId>;
275
276		/// The provider of the proposal operation.
277		type ProposalProvider: ProposalProvider<Self::AccountId, Self::Hash, Self::Proposal>;
278
279		/// Maximum number of proposals allowed to be active in parallel.
280		type MaxProposals: Get<ProposalIndex>;
281
282		/// The maximum number of Fellows supported by the pallet. Used for weight estimation.
283		///
284		/// NOTE:
285		/// + Benchmarks will need to be re-run and weights adjusted if this changes.
286		/// + This pallet assumes that dependencies keep to the limit without enforcing it.
287		type MaxFellows: Get<u32>;
288
289		/// The maximum number of Allies supported by the pallet. Used for weight estimation.
290		///
291		/// NOTE:
292		/// + Benchmarks will need to be re-run and weights adjusted if this changes.
293		/// + This pallet assumes that dependencies keep to the limit without enforcing it.
294		type MaxAllies: Get<u32>;
295
296		/// The maximum number of the unscrupulous items supported by the pallet.
297		#[pallet::constant]
298		type MaxUnscrupulousItems: Get<u32>;
299
300		/// The maximum length of a website URL.
301		#[pallet::constant]
302		type MaxWebsiteUrlLength: Get<u32>;
303
304		/// The deposit required for submitting candidacy.
305		#[pallet::constant]
306		type AllyDeposit: Get<BalanceOf<Self, I>>;
307
308		/// The maximum number of announcements.
309		#[pallet::constant]
310		type MaxAnnouncementsCount: Get<u32>;
311
312		/// The maximum number of members per member role.
313		#[pallet::constant]
314		type MaxMembersCount: Get<u32>;
315
316		/// Weight information for extrinsics in this pallet.
317		type WeightInfo: WeightInfo;
318
319		/// The number of blocks a member must wait between giving a retirement notice and retiring.
320		/// Supposed to be greater than time required to `kick_member`.
321		type RetirementPeriod: Get<BlockNumberFor<Self>>;
322	}
323
324	#[pallet::error]
325	pub enum Error<T, I = ()> {
326		/// The Alliance has not been initialized yet, therefore accounts cannot join it.
327		AllianceNotYetInitialized,
328		/// The Alliance has been initialized, therefore cannot be initialized again.
329		AllianceAlreadyInitialized,
330		/// Account is already a member.
331		AlreadyMember,
332		/// Account is not a member.
333		NotMember,
334		/// Account is not an ally.
335		NotAlly,
336		/// Account does not have voting rights.
337		NoVotingRights,
338		/// Account is already an elevated (fellow) member.
339		AlreadyElevated,
340		/// Item is already listed as unscrupulous.
341		AlreadyUnscrupulous,
342		/// Account has been deemed unscrupulous by the Alliance and is not welcome to join or be
343		/// nominated.
344		AccountNonGrata,
345		/// Item has not been deemed unscrupulous.
346		NotListedAsUnscrupulous,
347		/// The number of unscrupulous items exceeds `MaxUnscrupulousItems`.
348		TooManyUnscrupulousItems,
349		/// Length of website URL exceeds `MaxWebsiteUrlLength`.
350		TooLongWebsiteUrl,
351		/// Balance is insufficient for the required deposit.
352		InsufficientFunds,
353		/// The account's identity does not have display field and website field.
354		WithoutRequiredIdentityFields,
355		/// The account's identity has no good judgement.
356		WithoutGoodIdentityJudgement,
357		/// The proposal hash is not found.
358		MissingProposalHash,
359		/// The announcement is not found.
360		MissingAnnouncement,
361		/// Number of members exceeds `MaxMembersCount`.
362		TooManyMembers,
363		/// Number of announcements exceeds `MaxAnnouncementsCount`.
364		TooManyAnnouncements,
365		/// Invalid witness data given.
366		BadWitness,
367		/// Account already gave retirement notice
368		AlreadyRetiring,
369		/// Account did not give a retirement notice required to retire.
370		RetirementNoticeNotGiven,
371		/// Retirement period has not passed.
372		RetirementPeriodNotPassed,
373		/// Fellows must be provided to initialize the Alliance.
374		FellowsMissing,
375	}
376
377	#[pallet::event]
378	#[pallet::generate_deposit(pub(super) fn deposit_event)]
379	pub enum Event<T: Config<I>, I: 'static = ()> {
380		/// A new rule has been set.
381		NewRuleSet { rule: Cid },
382		/// A new announcement has been proposed.
383		Announced { announcement: Cid },
384		/// An on-chain announcement has been removed.
385		AnnouncementRemoved { announcement: Cid },
386		/// Some accounts have been initialized as members (fellows/allies).
387		MembersInitialized { fellows: Vec<T::AccountId>, allies: Vec<T::AccountId> },
388		/// An account has been added as an Ally and reserved its deposit.
389		NewAllyJoined {
390			ally: T::AccountId,
391			nominator: Option<T::AccountId>,
392			reserved: Option<BalanceOf<T, I>>,
393		},
394		/// An ally has been elevated to Fellow.
395		AllyElevated { ally: T::AccountId },
396		/// A member gave retirement notice and their retirement period started.
397		MemberRetirementPeriodStarted { member: T::AccountId },
398		/// A member has retired with its deposit unreserved.
399		MemberRetired { member: T::AccountId, unreserved: Option<BalanceOf<T, I>> },
400		/// A member has been kicked out with its deposit slashed.
401		MemberKicked { member: T::AccountId, slashed: Option<BalanceOf<T, I>> },
402		/// Accounts or websites have been added into the list of unscrupulous items.
403		UnscrupulousItemAdded { items: Vec<UnscrupulousItemOf<T, I>> },
404		/// Accounts or websites have been removed from the list of unscrupulous items.
405		UnscrupulousItemRemoved { items: Vec<UnscrupulousItemOf<T, I>> },
406		/// Alliance disbanded. Includes number deleted members and unreserved deposits.
407		AllianceDisbanded { fellow_members: u32, ally_members: u32, unreserved: u32 },
408		/// A Fellow abdicated their voting rights. They are now an Ally.
409		FellowAbdicated { fellow: T::AccountId },
410	}
411
412	#[pallet::genesis_config]
413	#[derive(frame_support::DefaultNoBound)]
414	pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
415		pub fellows: Vec<T::AccountId>,
416		pub allies: Vec<T::AccountId>,
417		#[serde(skip)]
418		pub phantom: PhantomData<(T, I)>,
419	}
420
421	#[pallet::genesis_build]
422	impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
423		fn build(&self) {
424			for m in self.fellows.iter().chain(self.allies.iter()) {
425				assert!(Pallet::<T, I>::has_identity(m).is_ok(), "Member does not set identity!");
426			}
427
428			if !self.fellows.is_empty() {
429				assert!(
430					!Pallet::<T, I>::has_member(MemberRole::Fellow),
431					"Fellows are already initialized!"
432				);
433				let members: BoundedVec<T::AccountId, T::MaxMembersCount> =
434					self.fellows.clone().try_into().expect("Too many genesis fellows");
435				Members::<T, I>::insert(MemberRole::Fellow, members);
436			}
437			if !self.allies.is_empty() {
438				assert!(
439					!Pallet::<T, I>::has_member(MemberRole::Ally),
440					"Allies are already initialized!"
441				);
442				assert!(
443					!self.fellows.is_empty(),
444					"Fellows must be provided to initialize the Alliance"
445				);
446				let members: BoundedVec<T::AccountId, T::MaxMembersCount> =
447					self.allies.clone().try_into().expect("Too many genesis allies");
448				Members::<T, I>::insert(MemberRole::Ally, members);
449			}
450
451			T::InitializeMembers::initialize_members(self.fellows.as_slice())
452		}
453	}
454
455	/// The IPFS CID of the alliance rule.
456	/// Fellows can propose a new rule with a super-majority.
457	#[pallet::storage]
458	pub type Rule<T: Config<I>, I: 'static = ()> = StorageValue<_, Cid, OptionQuery>;
459
460	/// The current IPFS CIDs of any announcements.
461	#[pallet::storage]
462	pub type Announcements<T: Config<I>, I: 'static = ()> =
463		StorageValue<_, BoundedVec<Cid, T::MaxAnnouncementsCount>, ValueQuery>;
464
465	/// Maps members to their candidacy deposit.
466	#[pallet::storage]
467	pub type DepositOf<T: Config<I>, I: 'static = ()> =
468		StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T, I>, OptionQuery>;
469
470	/// Maps member type to members of each type.
471	#[pallet::storage]
472	pub type Members<T: Config<I>, I: 'static = ()> = StorageMap<
473		_,
474		Twox64Concat,
475		MemberRole,
476		BoundedVec<T::AccountId, T::MaxMembersCount>,
477		ValueQuery,
478	>;
479
480	/// A set of members who gave a retirement notice. They can retire after the end of retirement
481	/// period stored as a future block number.
482	#[pallet::storage]
483	pub type RetiringMembers<T: Config<I>, I: 'static = ()> =
484		StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor<T>, OptionQuery>;
485
486	/// The current list of accounts deemed unscrupulous. These accounts non grata cannot submit
487	/// candidacy.
488	#[pallet::storage]
489	pub type UnscrupulousAccounts<T: Config<I>, I: 'static = ()> =
490		StorageValue<_, BoundedVec<T::AccountId, T::MaxUnscrupulousItems>, ValueQuery>;
491
492	/// The current list of websites deemed unscrupulous.
493	#[pallet::storage]
494	pub type UnscrupulousWebsites<T: Config<I>, I: 'static = ()> =
495		StorageValue<_, BoundedVec<UrlOf<T, I>, T::MaxUnscrupulousItems>, ValueQuery>;
496
497	#[pallet::call(weight(<T as Config<I>>::WeightInfo))]
498	impl<T: Config<I>, I: 'static> Pallet<T, I> {
499		/// Add a new proposal to be voted on.
500		///
501		/// Must be called by a Fellow.
502		#[pallet::call_index(0)]
503		#[pallet::weight(T::WeightInfo::propose_proposed(
504			*length_bound, // B
505			T::MaxFellows::get(), // M
506			T::MaxProposals::get(), // P2
507		))]
508		pub fn propose(
509			origin: OriginFor<T>,
510			#[pallet::compact] threshold: u32,
511			proposal: Box<<T as Config<I>>::Proposal>,
512			#[pallet::compact] length_bound: u32,
513		) -> DispatchResult {
514			let proposer = ensure_signed(origin)?;
515			ensure!(Self::has_voting_rights(&proposer), Error::<T, I>::NoVotingRights);
516
517			T::ProposalProvider::propose_proposal(proposer, threshold, proposal, length_bound)?;
518			Ok(())
519		}
520
521		/// Add an aye or nay vote for the sender to the given proposal.
522		///
523		/// Must be called by a Fellow.
524		#[pallet::call_index(1)]
525		#[pallet::weight(T::WeightInfo::vote(T::MaxFellows::get()))]
526		pub fn vote(
527			origin: OriginFor<T>,
528			proposal: T::Hash,
529			#[pallet::compact] index: ProposalIndex,
530			approve: bool,
531		) -> DispatchResult {
532			let who = ensure_signed(origin)?;
533			ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
534
535			T::ProposalProvider::vote_proposal(who, proposal, index, approve)?;
536			Ok(())
537		}
538
539		// Index 2 was `close_old_weight`; it was removed due to weights v1 deprecation.
540
541		/// Initialize the Alliance, onboard fellows and allies.
542		///
543		/// The Alliance must be empty, and the call must provide some founding members.
544		///
545		/// Must be called by the Root origin.
546		#[pallet::call_index(3)]
547		#[pallet::weight(T::WeightInfo::init_members(
548			fellows.len() as u32,
549			allies.len() as u32,
550		))]
551		pub fn init_members(
552			origin: OriginFor<T>,
553			fellows: Vec<T::AccountId>,
554			allies: Vec<T::AccountId>,
555		) -> DispatchResult {
556			ensure_root(origin)?;
557
558			ensure!(!fellows.is_empty(), Error::<T, I>::FellowsMissing);
559			ensure!(!Self::is_initialized(), Error::<T, I>::AllianceAlreadyInitialized);
560
561			let mut fellows: BoundedVec<T::AccountId, T::MaxMembersCount> =
562				fellows.try_into().map_err(|_| Error::<T, I>::TooManyMembers)?;
563			let mut allies: BoundedVec<T::AccountId, T::MaxMembersCount> =
564				allies.try_into().map_err(|_| Error::<T, I>::TooManyMembers)?;
565
566			for member in fellows.iter().chain(allies.iter()) {
567				Self::has_identity(member)?;
568			}
569
570			fellows.sort();
571			Members::<T, I>::insert(&MemberRole::Fellow, fellows.clone());
572			allies.sort();
573			Members::<T, I>::insert(&MemberRole::Ally, allies.clone());
574
575			let mut voteable_members = fellows.clone();
576			voteable_members.sort();
577
578			T::InitializeMembers::initialize_members(&voteable_members);
579
580			log::debug!(
581				target: LOG_TARGET,
582				"Initialize alliance fellows: {:?}, allies: {:?}",
583				fellows,
584				allies
585			);
586
587			Self::deposit_event(Event::MembersInitialized {
588				fellows: fellows.into(),
589				allies: allies.into(),
590			});
591			Ok(())
592		}
593
594		/// Disband the Alliance, remove all active members and unreserve deposits.
595		///
596		/// Witness data must be set.
597		#[pallet::call_index(4)]
598		#[pallet::weight(T::WeightInfo::disband(
599			witness.fellow_members,
600			witness.ally_members,
601			witness.fellow_members.saturating_add(witness.ally_members),
602		))]
603		pub fn disband(
604			origin: OriginFor<T>,
605			witness: DisbandWitness,
606		) -> DispatchResultWithPostInfo {
607			ensure_root(origin)?;
608
609			ensure!(!witness.is_zero(), Error::<T, I>::BadWitness);
610			ensure!(
611				Self::voting_members_count() <= witness.fellow_members,
612				Error::<T, I>::BadWitness
613			);
614			ensure!(Self::ally_members_count() <= witness.ally_members, Error::<T, I>::BadWitness);
615			ensure!(Self::is_initialized(), Error::<T, I>::AllianceNotYetInitialized);
616
617			let voting_members = Self::voting_members();
618			T::MembershipChanged::change_members_sorted(&[], &voting_members, &[]);
619
620			let ally_members = Self::members_of(MemberRole::Ally);
621			let mut unreserve_count: u32 = 0;
622			for member in voting_members.iter().chain(ally_members.iter()) {
623				if let Some(deposit) = DepositOf::<T, I>::take(&member) {
624					let err_amount = T::Currency::unreserve(&member, deposit);
625					debug_assert!(err_amount.is_zero());
626					unreserve_count += 1;
627				}
628			}
629
630			Members::<T, I>::remove(&MemberRole::Fellow);
631			Members::<T, I>::remove(&MemberRole::Ally);
632
633			Self::deposit_event(Event::AllianceDisbanded {
634				fellow_members: voting_members.len() as u32,
635				ally_members: ally_members.len() as u32,
636				unreserved: unreserve_count,
637			});
638
639			Ok(Some(T::WeightInfo::disband(
640				voting_members.len() as u32,
641				ally_members.len() as u32,
642				unreserve_count,
643			))
644			.into())
645		}
646
647		/// Set a new IPFS CID to the alliance rule.
648		#[pallet::call_index(5)]
649		pub fn set_rule(origin: OriginFor<T>, rule: Cid) -> DispatchResult {
650			T::AdminOrigin::ensure_origin(origin)?;
651
652			Rule::<T, I>::put(&rule);
653
654			Self::deposit_event(Event::NewRuleSet { rule });
655			Ok(())
656		}
657
658		/// Make an announcement of a new IPFS CID about alliance issues.
659		#[pallet::call_index(6)]
660		pub fn announce(origin: OriginFor<T>, announcement: Cid) -> DispatchResult {
661			T::AnnouncementOrigin::ensure_origin(origin)?;
662
663			let mut announcements = <Announcements<T, I>>::get();
664			announcements
665				.try_push(announcement.clone())
666				.map_err(|_| Error::<T, I>::TooManyAnnouncements)?;
667			<Announcements<T, I>>::put(announcements);
668
669			Self::deposit_event(Event::Announced { announcement });
670			Ok(())
671		}
672
673		/// Remove an announcement.
674		#[pallet::call_index(7)]
675		pub fn remove_announcement(origin: OriginFor<T>, announcement: Cid) -> DispatchResult {
676			T::AnnouncementOrigin::ensure_origin(origin)?;
677
678			let mut announcements = <Announcements<T, I>>::get();
679			let pos = announcements
680				.binary_search(&announcement)
681				.ok()
682				.ok_or(Error::<T, I>::MissingAnnouncement)?;
683			announcements.remove(pos);
684			<Announcements<T, I>>::put(announcements);
685
686			Self::deposit_event(Event::AnnouncementRemoved { announcement });
687			Ok(())
688		}
689
690		/// Submit oneself for candidacy. A fixed deposit is reserved.
691		#[pallet::call_index(8)]
692		pub fn join_alliance(origin: OriginFor<T>) -> DispatchResult {
693			let who = ensure_signed(origin)?;
694
695			// We don't want anyone to join as an Ally before the Alliance has been initialized via
696			// Root call. The reasons are two-fold:
697			//
698			// 1. There is no `Rule` or admission criteria, so the joiner would be an ally to
699			//    nought, and
700			// 2. It adds complexity to the initialization, namely deciding to overwrite accounts
701			//    that already joined as an Ally.
702			ensure!(Self::is_initialized(), Error::<T, I>::AllianceNotYetInitialized);
703
704			// Unscrupulous accounts are non grata.
705			ensure!(!Self::is_unscrupulous_account(&who), Error::<T, I>::AccountNonGrata);
706			ensure!(!Self::is_member(&who), Error::<T, I>::AlreadyMember);
707			// check user self or parent should has verified identity to reuse display name and
708			// website.
709			Self::has_identity(&who)?;
710
711			let deposit = T::AllyDeposit::get();
712			T::Currency::reserve(&who, deposit).map_err(|_| Error::<T, I>::InsufficientFunds)?;
713			<DepositOf<T, I>>::insert(&who, deposit);
714
715			Self::add_member(&who, MemberRole::Ally)?;
716
717			Self::deposit_event(Event::NewAllyJoined {
718				ally: who,
719				nominator: None,
720				reserved: Some(deposit),
721			});
722			Ok(())
723		}
724
725		/// A Fellow can nominate someone to join the alliance as an Ally. There is no deposit
726		/// required from the nominator or nominee.
727		#[pallet::call_index(9)]
728		pub fn nominate_ally(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
729			let nominator = ensure_signed(origin)?;
730			ensure!(Self::has_voting_rights(&nominator), Error::<T, I>::NoVotingRights);
731			let who = T::Lookup::lookup(who)?;
732
733			// Individual voting members cannot nominate accounts non grata.
734			ensure!(!Self::is_unscrupulous_account(&who), Error::<T, I>::AccountNonGrata);
735			ensure!(!Self::is_member(&who), Error::<T, I>::AlreadyMember);
736			// check user self or parent should has verified identity to reuse display name and
737			// website.
738			Self::has_identity(&who)?;
739
740			Self::add_member(&who, MemberRole::Ally)?;
741
742			Self::deposit_event(Event::NewAllyJoined {
743				ally: who,
744				nominator: Some(nominator),
745				reserved: None,
746			});
747			Ok(())
748		}
749
750		/// Elevate an Ally to Fellow.
751		#[pallet::call_index(10)]
752		pub fn elevate_ally(origin: OriginFor<T>, ally: AccountIdLookupOf<T>) -> DispatchResult {
753			T::MembershipManager::ensure_origin(origin)?;
754			let ally = T::Lookup::lookup(ally)?;
755			ensure!(Self::is_ally(&ally), Error::<T, I>::NotAlly);
756			ensure!(!Self::has_voting_rights(&ally), Error::<T, I>::AlreadyElevated);
757
758			Self::remove_member(&ally, MemberRole::Ally)?;
759			Self::add_member(&ally, MemberRole::Fellow)?;
760
761			Self::deposit_event(Event::AllyElevated { ally });
762			Ok(())
763		}
764
765		/// As a member, give a retirement notice and start a retirement period required to pass in
766		/// order to retire.
767		#[pallet::call_index(11)]
768		pub fn give_retirement_notice(origin: OriginFor<T>) -> DispatchResult {
769			let who = ensure_signed(origin)?;
770			let role = Self::member_role_of(&who).ok_or(Error::<T, I>::NotMember)?;
771			ensure!(role.ne(&MemberRole::Retiring), Error::<T, I>::AlreadyRetiring);
772
773			Self::remove_member(&who, role)?;
774			Self::add_member(&who, MemberRole::Retiring)?;
775			<RetiringMembers<T, I>>::insert(
776				&who,
777				frame_system::Pallet::<T>::block_number()
778					.saturating_add(T::RetirementPeriod::get()),
779			);
780
781			Self::deposit_event(Event::MemberRetirementPeriodStarted { member: who });
782			Ok(())
783		}
784
785		/// As a member, retire from the Alliance and unreserve the deposit.
786		///
787		/// This can only be done once you have called `give_retirement_notice` and the
788		/// `RetirementPeriod` has passed.
789		#[pallet::call_index(12)]
790		pub fn retire(origin: OriginFor<T>) -> DispatchResult {
791			let who = ensure_signed(origin)?;
792			let retirement_period_end = RetiringMembers::<T, I>::get(&who)
793				.ok_or(Error::<T, I>::RetirementNoticeNotGiven)?;
794			ensure!(
795				frame_system::Pallet::<T>::block_number() >= retirement_period_end,
796				Error::<T, I>::RetirementPeriodNotPassed
797			);
798
799			Self::remove_member(&who, MemberRole::Retiring)?;
800			<RetiringMembers<T, I>>::remove(&who);
801			let deposit = DepositOf::<T, I>::take(&who);
802			if let Some(deposit) = deposit {
803				let err_amount = T::Currency::unreserve(&who, deposit);
804				debug_assert!(err_amount.is_zero());
805			}
806			Self::deposit_event(Event::MemberRetired { member: who, unreserved: deposit });
807			Ok(())
808		}
809
810		/// Kick a member from the Alliance and slash its deposit.
811		#[pallet::call_index(13)]
812		pub fn kick_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
813			T::MembershipManager::ensure_origin(origin)?;
814			let member = T::Lookup::lookup(who)?;
815
816			let role = Self::member_role_of(&member).ok_or(Error::<T, I>::NotMember)?;
817			Self::remove_member(&member, role)?;
818			let deposit = DepositOf::<T, I>::take(member.clone());
819			if let Some(deposit) = deposit {
820				T::Slashed::on_unbalanced(T::Currency::slash_reserved(&member, deposit).0);
821			}
822
823			Self::deposit_event(Event::MemberKicked { member, slashed: deposit });
824			Ok(())
825		}
826
827		/// Add accounts or websites to the list of unscrupulous items.
828		#[pallet::call_index(14)]
829		#[pallet::weight(T::WeightInfo::add_unscrupulous_items(items.len() as u32, T::MaxWebsiteUrlLength::get()))]
830		pub fn add_unscrupulous_items(
831			origin: OriginFor<T>,
832			items: Vec<UnscrupulousItemOf<T, I>>,
833		) -> DispatchResult {
834			T::AnnouncementOrigin::ensure_origin(origin)?;
835
836			let mut accounts = vec![];
837			let mut webs = vec![];
838			for info in items.iter() {
839				ensure!(!Self::is_unscrupulous(info), Error::<T, I>::AlreadyUnscrupulous);
840				match info {
841					UnscrupulousItem::AccountId(who) => accounts.push(who.clone()),
842					UnscrupulousItem::Website(url) => {
843						ensure!(
844							url.len() as u32 <= T::MaxWebsiteUrlLength::get(),
845							Error::<T, I>::TooLongWebsiteUrl
846						);
847						webs.push(url.clone());
848					},
849				}
850			}
851
852			Self::do_add_unscrupulous_items(&mut accounts, &mut webs)?;
853			Self::deposit_event(Event::UnscrupulousItemAdded { items });
854			Ok(())
855		}
856
857		/// Deem some items no longer unscrupulous.
858		#[pallet::call_index(15)]
859		#[pallet::weight(<T as Config<I>>::WeightInfo::remove_unscrupulous_items(
860			items.len() as u32, T::MaxWebsiteUrlLength::get()
861		))]
862		pub fn remove_unscrupulous_items(
863			origin: OriginFor<T>,
864			items: Vec<UnscrupulousItemOf<T, I>>,
865		) -> DispatchResult {
866			T::AnnouncementOrigin::ensure_origin(origin)?;
867			let mut accounts = vec![];
868			let mut webs = vec![];
869			for info in items.iter() {
870				ensure!(Self::is_unscrupulous(info), Error::<T, I>::NotListedAsUnscrupulous);
871				match info {
872					UnscrupulousItem::AccountId(who) => accounts.push(who.clone()),
873					UnscrupulousItem::Website(url) => webs.push(url.clone()),
874				}
875			}
876			Self::do_remove_unscrupulous_items(&mut accounts, &mut webs)?;
877			Self::deposit_event(Event::UnscrupulousItemRemoved { items });
878			Ok(())
879		}
880
881		/// Close a vote that is either approved, disapproved, or whose voting period has ended.
882		///
883		/// Must be called by a Fellow.
884		#[pallet::call_index(16)]
885		#[pallet::weight({
886			let b = *length_bound;
887			let m = T::MaxFellows::get();
888			let p1 = *proposal_weight_bound;
889			let p2 = T::MaxProposals::get();
890			T::WeightInfo::close_early_approved(b, m, p2)
891				.max(T::WeightInfo::close_early_disapproved(m, p2))
892				.max(T::WeightInfo::close_approved(b, m, p2))
893				.max(T::WeightInfo::close_disapproved(m, p2))
894				.saturating_add(p1)
895		})]
896		pub fn close(
897			origin: OriginFor<T>,
898			proposal_hash: T::Hash,
899			#[pallet::compact] index: ProposalIndex,
900			proposal_weight_bound: Weight,
901			#[pallet::compact] length_bound: u32,
902		) -> DispatchResultWithPostInfo {
903			let who = ensure_signed(origin)?;
904			ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
905
906			Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
907		}
908
909		/// Abdicate one's position as a voting member and just be an Ally. May be used by Fellows
910		/// who do not want to leave the Alliance but do not have the capacity to participate
911		/// operationally for some time.
912		#[pallet::call_index(17)]
913		pub fn abdicate_fellow_status(origin: OriginFor<T>) -> DispatchResult {
914			let who = ensure_signed(origin)?;
915			let role = Self::member_role_of(&who).ok_or(Error::<T, I>::NotMember)?;
916			// Not applicable to members who are retiring or who are already Allies.
917			ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
918
919			Self::remove_member(&who, role)?;
920			Self::add_member(&who, MemberRole::Ally)?;
921
922			Self::deposit_event(Event::FellowAbdicated { fellow: who });
923			Ok(())
924		}
925	}
926}
927
928impl<T: Config<I>, I: 'static> Pallet<T, I> {
929	/// Check if the Alliance has been initialized.
930	fn is_initialized() -> bool {
931		Self::has_member(MemberRole::Fellow) || Self::has_member(MemberRole::Ally)
932	}
933
934	/// Check if a given role has any members.
935	fn has_member(role: MemberRole) -> bool {
936		Members::<T, I>::decode_len(role).unwrap_or_default() > 0
937	}
938
939	/// Look up the role, if any, of an account.
940	fn member_role_of(who: &T::AccountId) -> Option<MemberRole> {
941		Members::<T, I>::iter()
942			.find_map(|(r, members)| if members.contains(who) { Some(r) } else { None })
943	}
944
945	/// Check if a user is a alliance member.
946	pub fn is_member(who: &T::AccountId) -> bool {
947		Self::member_role_of(who).is_some()
948	}
949
950	/// Check if an account has a given role.
951	pub fn is_member_of(who: &T::AccountId, role: MemberRole) -> bool {
952		Members::<T, I>::get(role).contains(&who)
953	}
954
955	/// Check if an account is an Ally.
956	fn is_ally(who: &T::AccountId) -> bool {
957		Self::is_member_of(who, MemberRole::Ally)
958	}
959
960	/// Check if a member has voting rights.
961	fn has_voting_rights(who: &T::AccountId) -> bool {
962		Self::is_member_of(who, MemberRole::Fellow)
963	}
964
965	/// Count of ally members.
966	fn ally_members_count() -> u32 {
967		Members::<T, I>::decode_len(MemberRole::Ally).unwrap_or(0) as u32
968	}
969
970	/// Count of all members who have voting rights.
971	fn voting_members_count() -> u32 {
972		Members::<T, I>::decode_len(MemberRole::Fellow).unwrap_or(0) as u32
973	}
974
975	/// Get all members of a given role.
976	fn members_of(role: MemberRole) -> Vec<T::AccountId> {
977		Members::<T, I>::get(role).into_inner()
978	}
979
980	/// Collect all members who have voting rights into one list.
981	fn voting_members() -> Vec<T::AccountId> {
982		Self::members_of(MemberRole::Fellow)
983	}
984
985	/// Add a user to the sorted alliance member set.
986	fn add_member(who: &T::AccountId, role: MemberRole) -> DispatchResult {
987		<Members<T, I>>::try_mutate(role, |members| -> DispatchResult {
988			let pos = members.binary_search(who).err().ok_or(Error::<T, I>::AlreadyMember)?;
989			members
990				.try_insert(pos, who.clone())
991				.map_err(|_| Error::<T, I>::TooManyMembers)?;
992			Ok(())
993		})?;
994
995		if role == MemberRole::Fellow {
996			let members = Self::voting_members();
997			T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members[..]);
998		}
999		Ok(())
1000	}
1001
1002	/// Remove a user from the alliance member set.
1003	fn remove_member(who: &T::AccountId, role: MemberRole) -> DispatchResult {
1004		<Members<T, I>>::try_mutate(role, |members| -> DispatchResult {
1005			let pos = members.binary_search(who).ok().ok_or(Error::<T, I>::NotMember)?;
1006			members.remove(pos);
1007			Ok(())
1008		})?;
1009
1010		if role == MemberRole::Fellow {
1011			let members = Self::voting_members();
1012			T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]);
1013		}
1014		Ok(())
1015	}
1016
1017	/// Check if an item is listed as unscrupulous.
1018	fn is_unscrupulous(info: &UnscrupulousItemOf<T, I>) -> bool {
1019		match info {
1020			UnscrupulousItem::Website(url) => <UnscrupulousWebsites<T, I>>::get().contains(url),
1021			UnscrupulousItem::AccountId(who) => <UnscrupulousAccounts<T, I>>::get().contains(who),
1022		}
1023	}
1024
1025	/// Check if an account is listed as unscrupulous.
1026	fn is_unscrupulous_account(who: &T::AccountId) -> bool {
1027		<UnscrupulousAccounts<T, I>>::get().contains(who)
1028	}
1029
1030	/// Add item to the unscrupulous list.
1031	fn do_add_unscrupulous_items(
1032		new_accounts: &mut Vec<T::AccountId>,
1033		new_webs: &mut Vec<UrlOf<T, I>>,
1034	) -> DispatchResult {
1035		if !new_accounts.is_empty() {
1036			<UnscrupulousAccounts<T, I>>::try_mutate(|accounts| -> DispatchResult {
1037				accounts
1038					.try_append(new_accounts)
1039					.map_err(|_| Error::<T, I>::TooManyUnscrupulousItems)?;
1040				accounts.sort();
1041
1042				Ok(())
1043			})?;
1044		}
1045		if !new_webs.is_empty() {
1046			<UnscrupulousWebsites<T, I>>::try_mutate(|webs| -> DispatchResult {
1047				webs.try_append(new_webs).map_err(|_| Error::<T, I>::TooManyUnscrupulousItems)?;
1048				webs.sort();
1049
1050				Ok(())
1051			})?;
1052		}
1053
1054		Ok(())
1055	}
1056
1057	/// Remove item from the unscrupulous list.
1058	fn do_remove_unscrupulous_items(
1059		out_accounts: &mut Vec<T::AccountId>,
1060		out_webs: &mut Vec<UrlOf<T, I>>,
1061	) -> DispatchResult {
1062		if !out_accounts.is_empty() {
1063			<UnscrupulousAccounts<T, I>>::try_mutate(|accounts| -> DispatchResult {
1064				for who in out_accounts.iter() {
1065					let pos = accounts
1066						.binary_search(who)
1067						.ok()
1068						.ok_or(Error::<T, I>::NotListedAsUnscrupulous)?;
1069					accounts.remove(pos);
1070				}
1071				Ok(())
1072			})?;
1073		}
1074		if !out_webs.is_empty() {
1075			<UnscrupulousWebsites<T, I>>::try_mutate(|webs| -> DispatchResult {
1076				for web in out_webs.iter() {
1077					let pos = webs
1078						.binary_search(web)
1079						.ok()
1080						.ok_or(Error::<T, I>::NotListedAsUnscrupulous)?;
1081					webs.remove(pos);
1082				}
1083				Ok(())
1084			})?;
1085		}
1086		Ok(())
1087	}
1088
1089	fn has_identity(who: &T::AccountId) -> DispatchResult {
1090		let judgement = |who: &T::AccountId| -> DispatchResult {
1091			ensure!(
1092				T::IdentityVerifier::has_required_identities(who),
1093				Error::<T, I>::WithoutRequiredIdentityFields
1094			);
1095			ensure!(
1096				T::IdentityVerifier::has_good_judgement(who),
1097				Error::<T, I>::WithoutGoodIdentityJudgement
1098			);
1099			Ok(())
1100		};
1101
1102		let res = judgement(who);
1103		if res.is_err() {
1104			if let Some(parent) = T::IdentityVerifier::super_account_id(who) {
1105				return judgement(&parent)
1106			}
1107		}
1108		res
1109	}
1110
1111	fn do_close(
1112		proposal_hash: T::Hash,
1113		index: ProposalIndex,
1114		proposal_weight_bound: Weight,
1115		length_bound: u32,
1116	) -> DispatchResultWithPostInfo {
1117		let info = T::ProposalProvider::close_proposal(
1118			proposal_hash,
1119			index,
1120			proposal_weight_bound,
1121			length_bound,
1122		)?;
1123		Ok(info.into())
1124	}
1125}