1#![cfg_attr(not(feature = "std"), no_std)]
43
44extern crate alloc;
45
46use alloc::{boxed::Box, vec, vec::Vec};
47use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
48use core::{marker::PhantomData, result};
49use scale_info::TypeInfo;
50use sp_io::storage;
51use sp_runtime::{
52 traits::{Dispatchable, Hash},
53 Debug, DispatchError,
54};
55
56use frame_support::{
57 dispatch::{
58 DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, Pays, PostDispatchInfo,
59 },
60 ensure,
61 traits::{
62 Backing, ChangeMembers, Consideration, EnsureOrigin, EnsureOriginWithArg, Get, GetBacking,
63 InitializeMembers, MaybeConsideration, OriginTrait, StorageVersion,
64 },
65 weights::Weight,
66};
67
68#[cfg(any(feature = "try-runtime", test))]
69use sp_runtime::TryRuntimeError;
70
71#[cfg(test)]
72mod tests;
73
74#[cfg(feature = "runtime-benchmarks")]
75mod benchmarking;
76pub mod migrations;
77pub mod weights;
78
79pub use pallet::*;
80pub use weights::WeightInfo;
81
82const LOG_TARGET: &str = "runtime::collective";
83
84pub type ProposalIndex = u32;
86
87pub type MemberCount = u32;
92
93pub trait DefaultVote {
95 fn default_vote(
102 prime_vote: Option<bool>,
103 yes_votes: MemberCount,
104 no_votes: MemberCount,
105 len: MemberCount,
106 ) -> bool;
107}
108
109pub struct PrimeDefaultVote;
111
112impl DefaultVote for PrimeDefaultVote {
113 fn default_vote(
114 prime_vote: Option<bool>,
115 _yes_votes: MemberCount,
116 _no_votes: MemberCount,
117 _len: MemberCount,
118 ) -> bool {
119 prime_vote.unwrap_or(false)
120 }
121}
122
123pub struct MoreThanMajorityThenPrimeDefaultVote;
126
127impl DefaultVote for MoreThanMajorityThenPrimeDefaultVote {
128 fn default_vote(
129 prime_vote: Option<bool>,
130 yes_votes: MemberCount,
131 _no_votes: MemberCount,
132 len: MemberCount,
133 ) -> bool {
134 let more_than_majority = yes_votes * 2 > len;
135 more_than_majority || prime_vote.unwrap_or(false)
136 }
137}
138
139#[derive(
141 PartialEq, Eq, Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen,
142)]
143#[scale_info(skip_type_params(I))]
144#[codec(mel_bound(AccountId: MaxEncodedLen))]
145pub enum RawOrigin<AccountId, I> {
146 Members(MemberCount, MemberCount),
148 Member(AccountId),
150 _Phantom(PhantomData<I>),
152}
153
154impl<AccountId, I> GetBacking for RawOrigin<AccountId, I> {
155 fn get_backing(&self) -> Option<Backing> {
156 match self {
157 RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }),
158 _ => None,
159 }
160 }
161}
162
163#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo)]
165pub struct Votes<AccountId, BlockNumber> {
166 index: ProposalIndex,
168 threshold: MemberCount,
170 ayes: Vec<AccountId>,
172 nays: Vec<AccountId>,
174 end: BlockNumber,
176}
177
178#[doc = docify::embed!("src/tests.rs", deposit_types_with_linear_work)]
188#[doc = docify::embed!("src/tests.rs", deposit_types_with_geometric_work)]
191#[doc = docify::embed!("src/tests.rs", deposit_round_with_geometric_work)]
194pub mod deposit {
195 use core::marker::PhantomData;
196 use sp_core::Get;
197 use sp_runtime::{traits::Convert, FixedPointNumber, FixedU128, Saturating};
198
199 pub struct Constant<Deposit>(PhantomData<Deposit>);
202 impl<Deposit, Balance> Convert<u32, Balance> for Constant<Deposit>
203 where
204 Deposit: Get<Balance>,
205 Balance: frame_support::traits::tokens::Balance,
206 {
207 fn convert(_: u32) -> Balance {
208 Deposit::get()
209 }
210 }
211
212 pub struct Linear<Slope, Offset>(PhantomData<(Slope, Offset)>);
215 impl<Slope, Offset, Balance> Convert<u32, Balance> for Linear<Slope, Offset>
216 where
217 Slope: Get<u32>,
218 Offset: Get<Balance>,
219 Balance: frame_support::traits::tokens::Balance,
220 {
221 fn convert(proposal_count: u32) -> Balance {
222 let base: Balance = Slope::get().saturating_mul(proposal_count).into();
223 Offset::get().saturating_add(base)
224 }
225 }
226
227 pub struct Geometric<Ratio, Base>(PhantomData<(Ratio, Base)>);
230 impl<Ratio, Base, Balance> Convert<u32, Balance> for Geometric<Ratio, Base>
231 where
232 Ratio: Get<FixedU128>,
233 Base: Get<Balance>,
234 Balance: frame_support::traits::tokens::Balance,
235 {
236 fn convert(proposal_count: u32) -> Balance {
237 Ratio::get()
238 .saturating_pow(proposal_count as usize)
239 .saturating_mul_int(Base::get())
240 }
241 }
242
243 pub struct Round<Precision, Deposit>(PhantomData<(Precision, Deposit)>);
247 impl<Precision, Deposit, Balance> Convert<u32, Balance> for Round<Precision, Deposit>
248 where
249 Precision: Get<u32>,
250 Deposit: Convert<u32, Balance>,
251 Balance: frame_support::traits::tokens::Balance,
252 {
253 fn convert(proposal_count: u32) -> Balance {
254 let deposit = Deposit::convert(proposal_count);
255 if !deposit.is_zero() {
256 let factor: Balance =
257 Balance::from(10u32).saturating_pow(Precision::get() as usize);
258 if factor > deposit {
259 deposit
260 } else {
261 (deposit / factor) * factor
262 }
263 } else {
264 deposit
265 }
266 }
267 }
268
269 pub struct Stepped<Period, Step>(PhantomData<(Period, Step)>);
271 impl<Period, Step, Balance> Convert<u32, Balance> for Stepped<Period, Step>
272 where
273 Period: Get<u32>,
274 Step: Convert<u32, Balance>,
275 Balance: frame_support::traits::tokens::Balance,
276 {
277 fn convert(proposal_count: u32) -> Balance {
278 let step_num = proposal_count / Period::get();
279 Step::convert(step_num)
280 }
281 }
282
283 pub struct Delayed<Delay, Deposit>(PhantomData<(Delay, Deposit)>);
285 impl<Delay, Deposit, Balance> Convert<u32, Balance> for Delayed<Delay, Deposit>
286 where
287 Delay: Get<u32>,
288 Deposit: Convert<u32, Balance>,
289 Balance: frame_support::traits::tokens::Balance,
290 {
291 fn convert(proposal_count: u32) -> Balance {
292 let delay = Delay::get();
293 if delay > proposal_count {
294 return Balance::zero();
295 }
296 let pos = proposal_count.saturating_sub(delay);
297 Deposit::convert(pos)
298 }
299 }
300
301 pub struct WithCeil<Ceil, Deposit>(PhantomData<(Ceil, Deposit)>);
303 impl<Ceil, Deposit, Balance> Convert<u32, Balance> for WithCeil<Ceil, Deposit>
304 where
305 Ceil: Get<Balance>,
306 Deposit: Convert<u32, Balance>,
307 Balance: frame_support::traits::tokens::Balance,
308 {
309 fn convert(proposal_count: u32) -> Balance {
310 Deposit::convert(proposal_count).min(Ceil::get())
311 }
312 }
313}
314
315#[frame_support::pallet]
316pub mod pallet {
317 use super::*;
318 use frame_support::pallet_prelude::*;
319 use frame_system::pallet_prelude::*;
320
321 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
323
324 #[pallet::pallet]
325 #[pallet::storage_version(STORAGE_VERSION)]
326 #[pallet::without_storage_info]
327 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
328
329 #[pallet::config]
330 pub trait Config<I: 'static = ()>: frame_system::Config {
331 type RuntimeOrigin: From<RawOrigin<Self::AccountId, I>>;
333
334 type Proposal: Parameter
336 + Dispatchable<
337 RuntimeOrigin = <Self as Config<I>>::RuntimeOrigin,
338 PostInfo = PostDispatchInfo,
339 > + From<frame_system::Call<Self>>
340 + GetDispatchInfo;
341
342 #[allow(deprecated)]
344 type RuntimeEvent: From<Event<Self, I>>
345 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
346
347 type MotionDuration: Get<BlockNumberFor<Self>>;
349
350 type MaxProposals: Get<ProposalIndex>;
352
353 type MaxMembers: Get<MemberCount>;
359
360 type DefaultVote: DefaultVote;
362
363 type WeightInfo: WeightInfo;
365
366 type SetMembersOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
368
369 #[pallet::constant]
371 type MaxProposalWeight: Get<Weight>;
372
373 type DisapproveOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
376
377 type KillOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
382
383 type Consideration: MaybeConsideration<Self::AccountId, u32>;
393 }
394
395 #[pallet::genesis_config]
396 #[derive(frame_support::DefaultNoBound)]
397 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
398 #[serde(skip)]
399 pub phantom: PhantomData<I>,
400 pub members: Vec<T::AccountId>,
401 }
402
403 #[pallet::genesis_build]
404 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
405 fn build(&self) {
406 use alloc::collections::btree_set::BTreeSet;
407 let members_set: BTreeSet<_> = self.members.iter().collect();
408 assert_eq!(
409 members_set.len(),
410 self.members.len(),
411 "Members cannot contain duplicate accounts."
412 );
413 assert!(
414 self.members.len() <= T::MaxMembers::get() as usize,
415 "Members length cannot exceed MaxMembers.",
416 );
417
418 Pallet::<T, I>::initialize_members(&self.members)
419 }
420 }
421
422 #[pallet::origin]
424 pub type Origin<T, I = ()> = RawOrigin<<T as frame_system::Config>::AccountId, I>;
425
426 #[pallet::storage]
428 pub type Proposals<T: Config<I>, I: 'static = ()> =
429 StorageValue<_, BoundedVec<T::Hash, T::MaxProposals>, ValueQuery>;
430
431 #[pallet::storage]
433 pub type ProposalOf<T: Config<I>, I: 'static = ()> =
434 StorageMap<_, Identity, T::Hash, <T as Config<I>>::Proposal, OptionQuery>;
435
436 #[pallet::storage]
441 pub type CostOf<T: Config<I>, I: 'static = ()> =
442 StorageMap<_, Identity, T::Hash, (T::AccountId, T::Consideration), OptionQuery>;
443
444 #[pallet::storage]
446 pub type Voting<T: Config<I>, I: 'static = ()> =
447 StorageMap<_, Identity, T::Hash, Votes<T::AccountId, BlockNumberFor<T>>, OptionQuery>;
448
449 #[pallet::storage]
451 pub type ProposalCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
452
453 #[pallet::storage]
455 pub type Members<T: Config<I>, I: 'static = ()> =
456 StorageValue<_, Vec<T::AccountId>, ValueQuery>;
457
458 #[pallet::storage]
460 pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
461
462 #[pallet::event]
463 #[pallet::generate_deposit(pub(super) fn deposit_event)]
464 pub enum Event<T: Config<I>, I: 'static = ()> {
465 Proposed {
468 account: T::AccountId,
469 proposal_index: ProposalIndex,
470 proposal_hash: T::Hash,
471 threshold: MemberCount,
472 },
473 Voted {
476 account: T::AccountId,
477 proposal_hash: T::Hash,
478 voted: bool,
479 yes: MemberCount,
480 no: MemberCount,
481 },
482 Approved { proposal_hash: T::Hash },
484 Disapproved { proposal_hash: T::Hash },
486 Executed { proposal_hash: T::Hash, result: DispatchResult },
488 MemberExecuted { proposal_hash: T::Hash, result: DispatchResult },
490 Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount },
492 Killed { proposal_hash: T::Hash },
494 ProposalCostBurned { proposal_hash: T::Hash, who: T::AccountId },
496 ProposalCostReleased { proposal_hash: T::Hash, who: T::AccountId },
498 }
499
500 #[pallet::error]
501 pub enum Error<T, I = ()> {
502 NotMember,
504 DuplicateProposal,
506 ProposalMissing,
508 WrongIndex,
510 DuplicateVote,
512 AlreadyInitialized,
514 TooEarly,
516 TooManyProposals,
518 WrongProposalWeight,
520 WrongProposalLength,
522 PrimeAccountNotMember,
524 ProposalActive,
526 }
527
528 #[pallet::hooks]
529 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
530 #[cfg(feature = "try-runtime")]
531 fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
532 Self::do_try_state()
533 }
534 }
535
536 #[pallet::composite_enum]
538 pub enum HoldReason<I: 'static = ()> {
539 #[codec(index = 0)]
541 ProposalSubmission,
542 }
543
544 #[pallet::call]
546 impl<T: Config<I>, I: 'static> Pallet<T, I> {
547 #[pallet::call_index(0)]
572 #[pallet::weight((
573 T::WeightInfo::set_members(
574 *old_count, new_members.len() as u32, T::MaxProposals::get() ),
578 DispatchClass::Operational
579 ))]
580 pub fn set_members(
581 origin: OriginFor<T>,
582 new_members: Vec<T::AccountId>,
583 prime: Option<T::AccountId>,
584 old_count: MemberCount,
585 ) -> DispatchResultWithPostInfo {
586 T::SetMembersOrigin::ensure_origin(origin)?;
587 if new_members.len() > T::MaxMembers::get() as usize {
588 log::error!(
589 target: LOG_TARGET,
590 "New members count ({}) exceeds maximum amount of members expected ({}).",
591 new_members.len(),
592 T::MaxMembers::get(),
593 );
594 }
595
596 let old = Members::<T, I>::get();
597 if old.len() > old_count as usize {
598 log::warn!(
599 target: LOG_TARGET,
600 "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})",
601 old_count,
602 old.len(),
603 );
604 }
605 if let Some(p) = &prime {
606 ensure!(new_members.contains(p), Error::<T, I>::PrimeAccountNotMember);
607 }
608 let mut new_members = new_members;
609 new_members.sort();
610 <Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members, &old);
611 Prime::<T, I>::set(prime);
612
613 Ok(Some(T::WeightInfo::set_members(
614 old.len() as u32, new_members.len() as u32, T::MaxProposals::get(), ))
618 .into())
619 }
620
621 #[pallet::call_index(1)]
631 #[pallet::weight((
632 T::WeightInfo::execute(
633 *length_bound, T::MaxMembers::get(), ).saturating_add(proposal.get_dispatch_info().call_weight), DispatchClass::Operational
637 ))]
638 pub fn execute(
639 origin: OriginFor<T>,
640 proposal: Box<<T as Config<I>>::Proposal>,
641 #[pallet::compact] length_bound: u32,
642 ) -> DispatchResultWithPostInfo {
643 let who = ensure_signed(origin)?;
644 let members = Members::<T, I>::get();
645 ensure!(members.contains(&who), Error::<T, I>::NotMember);
646 let proposal_len = proposal.encoded_size();
647 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
648
649 let proposal_hash = T::Hashing::hash_of(&proposal);
650 let result = proposal.dispatch(RawOrigin::Member(who).into());
651 Self::deposit_event(Event::MemberExecuted {
652 proposal_hash,
653 result: result.map(|_| ()).map_err(|e| e.error),
654 });
655
656 Ok(get_result_weight(result)
657 .map(|w| {
658 T::WeightInfo::execute(
659 proposal_len as u32, members.len() as u32, )
662 .saturating_add(w) })
664 .into())
665 }
666
667 #[pallet::call_index(2)]
682 #[pallet::weight((
683 if *threshold < 2 {
684 T::WeightInfo::propose_execute(
685 *length_bound, T::MaxMembers::get(), ).saturating_add(proposal.get_dispatch_info().call_weight) } else {
689 T::WeightInfo::propose_proposed(
690 *length_bound, T::MaxMembers::get(), T::MaxProposals::get(), )
694 },
695 DispatchClass::Operational
696 ))]
697 pub fn propose(
698 origin: OriginFor<T>,
699 #[pallet::compact] threshold: MemberCount,
700 proposal: Box<<T as Config<I>>::Proposal>,
701 #[pallet::compact] length_bound: u32,
702 ) -> DispatchResultWithPostInfo {
703 let who = ensure_signed(origin)?;
704 let members = Members::<T, I>::get();
705 ensure!(members.contains(&who), Error::<T, I>::NotMember);
706
707 if threshold < 2 {
708 let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?;
709
710 Ok(get_result_weight(result)
711 .map(|w| {
712 T::WeightInfo::propose_execute(
713 proposal_len as u32, members.len() as u32, )
716 .saturating_add(w) })
718 .into())
719 } else {
720 let (proposal_len, active_proposals) =
721 Self::do_propose_proposed(who, threshold, proposal, length_bound)?;
722
723 Ok(Some(T::WeightInfo::propose_proposed(
724 proposal_len as u32, members.len() as u32, active_proposals, ))
728 .into())
729 }
730 }
731
732 #[pallet::call_index(3)]
742 #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))]
743 pub fn vote(
744 origin: OriginFor<T>,
745 proposal: T::Hash,
746 #[pallet::compact] index: ProposalIndex,
747 approve: bool,
748 ) -> DispatchResultWithPostInfo {
749 let who = ensure_signed(origin)?;
750 let members = Members::<T, I>::get();
751 ensure!(members.contains(&who), Error::<T, I>::NotMember);
752
753 let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?;
755
756 if is_account_voting_first_time {
757 Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into())
758 } else {
759 Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into())
760 }
761 }
762
763 #[pallet::call_index(5)]
776 #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))]
777 pub fn disapprove_proposal(
778 origin: OriginFor<T>,
779 proposal_hash: T::Hash,
780 ) -> DispatchResultWithPostInfo {
781 T::DisapproveOrigin::ensure_origin(origin)?;
782 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
783 Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into())
784 }
785
786 #[pallet::call_index(6)]
811 #[pallet::weight((
812 {
813 let b = *length_bound;
814 let m = T::MaxMembers::get();
815 let p1 = *proposal_weight_bound;
816 let p2 = T::MaxProposals::get();
817 T::WeightInfo::close_early_approved(b, m, p2)
818 .max(T::WeightInfo::close_early_disapproved(m, p2))
819 .max(T::WeightInfo::close_approved(b, m, p2))
820 .max(T::WeightInfo::close_disapproved(m, p2))
821 .saturating_add(p1)
822 },
823 DispatchClass::Operational
824 ))]
825 pub fn close(
826 origin: OriginFor<T>,
827 proposal_hash: T::Hash,
828 #[pallet::compact] index: ProposalIndex,
829 proposal_weight_bound: Weight,
830 #[pallet::compact] length_bound: u32,
831 ) -> DispatchResultWithPostInfo {
832 ensure_signed(origin)?;
833
834 Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
835 }
836
837 #[pallet::call_index(7)]
845 #[pallet::weight(T::WeightInfo::kill(1, T::MaxProposals::get()))]
846 pub fn kill(origin: OriginFor<T>, proposal_hash: T::Hash) -> DispatchResultWithPostInfo {
847 T::KillOrigin::ensure_origin(origin)?;
848 ensure!(
849 ProposalOf::<T, I>::get(&proposal_hash).is_some(),
850 Error::<T, I>::ProposalMissing
851 );
852 let burned = if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
853 cost.burn(&who);
854 Self::deposit_event(Event::ProposalCostBurned { proposal_hash, who });
855 true
856 } else {
857 false
858 };
859 let proposal_count = Self::remove_proposal(proposal_hash);
860
861 Self::deposit_event(Event::Killed { proposal_hash });
862
863 Ok(Some(T::WeightInfo::kill(burned as u32, proposal_count)).into())
864 }
865
866 #[pallet::call_index(8)]
876 #[pallet::weight(T::WeightInfo::release_proposal_cost())]
877 pub fn release_proposal_cost(
878 origin: OriginFor<T>,
879 proposal_hash: T::Hash,
880 ) -> DispatchResult {
881 ensure_signed_or_root(origin)?;
882 ensure!(
883 ProposalOf::<T, I>::get(&proposal_hash).is_none(),
884 Error::<T, I>::ProposalActive
885 );
886 if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
887 cost.drop(&who)?;
888 Self::deposit_event(Event::ProposalCostReleased { proposal_hash, who });
889 }
890
891 Ok(())
892 }
893 }
894}
895
896fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
900 match result {
901 Ok(post_info) => post_info.actual_weight,
902 Err(err) => err.post_info.actual_weight,
903 }
904}
905
906impl<T: Config<I>, I: 'static> Pallet<T, I> {
907 pub fn is_member(who: &T::AccountId) -> bool {
909 Members::<T, I>::get().contains(who)
912 }
913
914 pub fn do_propose_execute(
916 proposal: Box<<T as Config<I>>::Proposal>,
917 length_bound: MemberCount,
918 ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> {
919 let proposal_len = proposal.encoded_size();
920 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
921 let proposal_weight = proposal.get_dispatch_info().call_weight;
922 ensure!(
923 proposal_weight.all_lte(T::MaxProposalWeight::get()),
924 Error::<T, I>::WrongProposalWeight
925 );
926
927 let proposal_hash = T::Hashing::hash_of(&proposal);
928 ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
929
930 let seats = Members::<T, I>::get().len() as MemberCount;
931 let result = proposal.dispatch(RawOrigin::Members(1, seats).into());
932 Self::deposit_event(Event::Executed {
933 proposal_hash,
934 result: result.map(|_| ()).map_err(|e| e.error),
935 });
936 Ok((proposal_len as u32, result))
937 }
938
939 pub fn do_propose_proposed(
941 who: T::AccountId,
942 threshold: MemberCount,
943 proposal: Box<<T as Config<I>>::Proposal>,
944 length_bound: MemberCount,
945 ) -> Result<(u32, u32), DispatchError> {
946 let proposal_len = proposal.encoded_size();
947 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
948 let proposal_weight = proposal.get_dispatch_info().call_weight;
949 ensure!(
950 proposal_weight.all_lte(T::MaxProposalWeight::get()),
951 Error::<T, I>::WrongProposalWeight
952 );
953
954 let proposal_hash = T::Hashing::hash_of(&proposal);
955 ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
956
957 let active_proposals =
958 <Proposals<T, I>>::try_mutate(|proposals| -> Result<usize, DispatchError> {
959 proposals.try_push(proposal_hash).map_err(|_| Error::<T, I>::TooManyProposals)?;
960 Ok(proposals.len())
961 })?;
962
963 let cost = T::Consideration::new(&who, active_proposals as u32 - 1)?;
964 if !cost.is_none() {
965 <CostOf<T, I>>::insert(proposal_hash, (who.clone(), cost));
966 }
967
968 let index = ProposalCount::<T, I>::get();
969
970 <ProposalCount<T, I>>::mutate(|i| *i += 1);
971 <ProposalOf<T, I>>::insert(proposal_hash, proposal);
972 let votes = {
973 let end = frame_system::Pallet::<T>::block_number() + T::MotionDuration::get();
974 Votes { index, threshold, ayes: vec![], nays: vec![], end }
975 };
976 <Voting<T, I>>::insert(proposal_hash, votes);
977
978 Self::deposit_event(Event::Proposed {
979 account: who,
980 proposal_index: index,
981 proposal_hash,
982 threshold,
983 });
984 Ok((proposal_len as u32, active_proposals as u32))
985 }
986
987 pub fn do_vote(
990 who: T::AccountId,
991 proposal: T::Hash,
992 index: ProposalIndex,
993 approve: bool,
994 ) -> Result<bool, DispatchError> {
995 let mut voting = Voting::<T, I>::get(&proposal).ok_or(Error::<T, I>::ProposalMissing)?;
996 ensure!(voting.index == index, Error::<T, I>::WrongIndex);
997
998 let position_yes = voting.ayes.iter().position(|a| a == &who);
999 let position_no = voting.nays.iter().position(|a| a == &who);
1000
1001 let is_account_voting_first_time = position_yes.is_none() && position_no.is_none();
1003
1004 if approve {
1005 if position_yes.is_none() {
1006 voting.ayes.push(who.clone());
1007 } else {
1008 return Err(Error::<T, I>::DuplicateVote.into())
1009 }
1010 if let Some(pos) = position_no {
1011 voting.nays.swap_remove(pos);
1012 }
1013 } else {
1014 if position_no.is_none() {
1015 voting.nays.push(who.clone());
1016 } else {
1017 return Err(Error::<T, I>::DuplicateVote.into())
1018 }
1019 if let Some(pos) = position_yes {
1020 voting.ayes.swap_remove(pos);
1021 }
1022 }
1023
1024 let yes_votes = voting.ayes.len() as MemberCount;
1025 let no_votes = voting.nays.len() as MemberCount;
1026 Self::deposit_event(Event::Voted {
1027 account: who,
1028 proposal_hash: proposal,
1029 voted: approve,
1030 yes: yes_votes,
1031 no: no_votes,
1032 });
1033
1034 Voting::<T, I>::insert(&proposal, voting);
1035
1036 Ok(is_account_voting_first_time)
1037 }
1038
1039 pub fn do_close(
1041 proposal_hash: T::Hash,
1042 index: ProposalIndex,
1043 proposal_weight_bound: Weight,
1044 length_bound: u32,
1045 ) -> DispatchResultWithPostInfo {
1046 let voting = Voting::<T, I>::get(&proposal_hash).ok_or(Error::<T, I>::ProposalMissing)?;
1047 ensure!(voting.index == index, Error::<T, I>::WrongIndex);
1048
1049 let mut no_votes = voting.nays.len() as MemberCount;
1050 let mut yes_votes = voting.ayes.len() as MemberCount;
1051 let seats = Members::<T, I>::get().len() as MemberCount;
1052 let approved = yes_votes >= voting.threshold;
1053 let disapproved = seats.saturating_sub(no_votes) < voting.threshold;
1054 if approved {
1056 let (proposal, len) = Self::validate_and_get_proposal(
1057 &proposal_hash,
1058 length_bound,
1059 proposal_weight_bound,
1060 )?;
1061 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1062 let (proposal_weight, proposal_count) =
1063 Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1064 return Ok((
1065 Some(
1066 T::WeightInfo::close_early_approved(len as u32, seats, proposal_count)
1067 .saturating_add(proposal_weight),
1068 ),
1069 Pays::Yes,
1070 )
1071 .into())
1072 } else if disapproved {
1073 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1074 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1075 return Ok((
1076 Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)),
1077 Pays::No,
1078 )
1079 .into())
1080 }
1081
1082 ensure!(frame_system::Pallet::<T>::block_number() >= voting.end, Error::<T, I>::TooEarly);
1084
1085 let prime_vote = Prime::<T, I>::get().map(|who| voting.ayes.iter().any(|a| a == &who));
1086
1087 let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats);
1089
1090 let abstentions = seats - (yes_votes + no_votes);
1091 match default {
1092 true => yes_votes += abstentions,
1093 false => no_votes += abstentions,
1094 }
1095 let approved = yes_votes >= voting.threshold;
1096
1097 if approved {
1098 let (proposal, len) = Self::validate_and_get_proposal(
1099 &proposal_hash,
1100 length_bound,
1101 proposal_weight_bound,
1102 )?;
1103 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1104 let (proposal_weight, proposal_count) =
1105 Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1106 Ok((
1107 Some(
1108 T::WeightInfo::close_approved(len as u32, seats, proposal_count)
1109 .saturating_add(proposal_weight),
1110 ),
1111 Pays::Yes,
1112 )
1113 .into())
1114 } else {
1115 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1116 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1117 Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into())
1118 }
1119 }
1120
1121 fn validate_and_get_proposal(
1126 hash: &T::Hash,
1127 length_bound: u32,
1128 weight_bound: Weight,
1129 ) -> Result<(<T as Config<I>>::Proposal, usize), DispatchError> {
1130 let key = ProposalOf::<T, I>::hashed_key_for(hash);
1131 let proposal_len =
1133 storage::read(&key, &mut [0; 0], 0).ok_or(Error::<T, I>::ProposalMissing)?;
1134 ensure!(proposal_len <= length_bound, Error::<T, I>::WrongProposalLength);
1135 let proposal = ProposalOf::<T, I>::get(hash).ok_or(Error::<T, I>::ProposalMissing)?;
1136 let proposal_weight = proposal.get_dispatch_info().call_weight;
1137 ensure!(proposal_weight.all_lte(weight_bound), Error::<T, I>::WrongProposalWeight);
1138 Ok((proposal, proposal_len as usize))
1139 }
1140
1141 fn do_approve_proposal(
1156 seats: MemberCount,
1157 yes_votes: MemberCount,
1158 proposal_hash: T::Hash,
1159 proposal: <T as Config<I>>::Proposal,
1160 ) -> (Weight, u32) {
1161 Self::deposit_event(Event::Approved { proposal_hash });
1162
1163 let dispatch_weight = proposal.get_dispatch_info().call_weight;
1164 let origin = RawOrigin::Members(yes_votes, seats).into();
1165 let result = proposal.dispatch(origin);
1166 Self::deposit_event(Event::Executed {
1167 proposal_hash,
1168 result: result.map(|_| ()).map_err(|e| e.error),
1169 });
1170 let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); let proposal_count = Self::remove_proposal(proposal_hash);
1174 (proposal_weight, proposal_count)
1175 }
1176
1177 pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 {
1179 Self::deposit_event(Event::Disapproved { proposal_hash });
1181 Self::remove_proposal(proposal_hash)
1182 }
1183
1184 fn remove_proposal(proposal_hash: T::Hash) -> u32 {
1186 ProposalOf::<T, I>::remove(&proposal_hash);
1188 Voting::<T, I>::remove(&proposal_hash);
1189 let num_proposals = Proposals::<T, I>::mutate(|proposals| {
1190 proposals.retain(|h| h != &proposal_hash);
1191 proposals.len() + 1 });
1193 num_proposals as u32
1194 }
1195
1196 #[cfg(any(feature = "try-runtime", test))]
1225 fn do_try_state() -> Result<(), TryRuntimeError> {
1226 Proposals::<T, I>::get().into_iter().try_for_each(
1227 |proposal| -> Result<(), TryRuntimeError> {
1228 ensure!(
1229 ProposalOf::<T, I>::get(proposal).is_some(),
1230 "Proposal hash from `Proposals` is not found inside the `ProposalOf` mapping."
1231 );
1232 Ok(())
1233 },
1234 )?;
1235
1236 ensure!(
1237 Proposals::<T, I>::get().into_iter().count() <= ProposalCount::<T, I>::get() as usize,
1238 "The actual number of proposals is greater than `ProposalCount`"
1239 );
1240 ensure!(
1241 Proposals::<T, I>::get().into_iter().count() == <ProposalOf<T, I>>::iter_keys().count(),
1242 "Proposal count inside `Proposals` is not equal to the proposal count in `ProposalOf`"
1243 );
1244
1245 Proposals::<T, I>::get().into_iter().try_for_each(
1246 |proposal| -> Result<(), TryRuntimeError> {
1247 if let Some(votes) = Voting::<T, I>::get(proposal) {
1248 let ayes = votes.ayes.len();
1249 let nays = votes.nays.len();
1250
1251 ensure!(
1252 ayes.saturating_add(nays) <= T::MaxMembers::get() as usize,
1253 "The sum of ayes and nays is greater than `MaxMembers`"
1254 );
1255 }
1256 Ok(())
1257 },
1258 )?;
1259
1260 let mut proposal_indices = vec![];
1261 Proposals::<T, I>::get().into_iter().try_for_each(
1262 |proposal| -> Result<(), TryRuntimeError> {
1263 if let Some(votes) = Voting::<T, I>::get(proposal) {
1264 let proposal_index = votes.index;
1265 ensure!(
1266 !proposal_indices.contains(&proposal_index),
1267 "The proposal index is not unique."
1268 );
1269 proposal_indices.push(proposal_index);
1270 }
1271 Ok(())
1272 },
1273 )?;
1274
1275 <Voting<T, I>>::iter_keys().try_for_each(
1276 |proposal_hash| -> Result<(), TryRuntimeError> {
1277 ensure!(
1278 Proposals::<T, I>::get().contains(&proposal_hash),
1279 "`Proposals` doesn't contain the proposal hash from the `Voting` storage map."
1280 );
1281 Ok(())
1282 },
1283 )?;
1284
1285 ensure!(
1286 Members::<T, I>::get().len() <= T::MaxMembers::get() as usize,
1287 "The member count is greater than `MaxMembers`."
1288 );
1289
1290 ensure!(
1291 Members::<T, I>::get().windows(2).all(|members| members[0] <= members[1]),
1292 "The members are not sorted by value."
1293 );
1294
1295 if let Some(prime) = Prime::<T, I>::get() {
1296 ensure!(Members::<T, I>::get().contains(&prime), "Prime account is not a member.");
1297 }
1298
1299 Ok(())
1300 }
1301}
1302
1303impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> {
1304 fn change_members_sorted(
1315 _incoming: &[T::AccountId],
1316 outgoing: &[T::AccountId],
1317 new: &[T::AccountId],
1318 ) {
1319 if new.len() > T::MaxMembers::get() as usize {
1320 log::error!(
1321 target: LOG_TARGET,
1322 "New members count ({}) exceeds maximum amount of members expected ({}).",
1323 new.len(),
1324 T::MaxMembers::get(),
1325 );
1326 }
1327 let mut outgoing = outgoing.to_vec();
1329 outgoing.sort();
1330 for h in Proposals::<T, I>::get().into_iter() {
1331 <Voting<T, I>>::mutate(h, |v| {
1332 if let Some(mut votes) = v.take() {
1333 votes.ayes = votes
1334 .ayes
1335 .into_iter()
1336 .filter(|i| outgoing.binary_search(i).is_err())
1337 .collect();
1338 votes.nays = votes
1339 .nays
1340 .into_iter()
1341 .filter(|i| outgoing.binary_search(i).is_err())
1342 .collect();
1343 *v = Some(votes);
1344 }
1345 });
1346 }
1347 Members::<T, I>::put(new);
1348 Prime::<T, I>::kill();
1349 }
1350
1351 fn set_prime(prime: Option<T::AccountId>) {
1352 Prime::<T, I>::set(prime);
1353 }
1354
1355 fn get_prime() -> Option<T::AccountId> {
1356 Prime::<T, I>::get()
1357 }
1358}
1359
1360impl<T: Config<I>, I: 'static> InitializeMembers<T::AccountId> for Pallet<T, I> {
1361 fn initialize_members(members: &[T::AccountId]) {
1362 if !members.is_empty() {
1363 assert!(<Members<T, I>>::get().is_empty(), "Members are already initialized!");
1364 let mut members = members.to_vec();
1365 members.sort();
1366 <Members<T, I>>::put(members);
1367 }
1368 }
1369}
1370
1371pub fn ensure_members<OuterOrigin, AccountId, I>(
1374 o: OuterOrigin,
1375 n: MemberCount,
1376) -> result::Result<MemberCount, &'static str>
1377where
1378 OuterOrigin: Into<result::Result<RawOrigin<AccountId, I>, OuterOrigin>>,
1379{
1380 match o.into() {
1381 Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n),
1382 _ => Err("bad origin: expected to be a threshold number of members"),
1383 }
1384}
1385
1386pub struct EnsureMember<AccountId, I: 'static>(PhantomData<(AccountId, I)>);
1387impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone> EnsureOrigin<O>
1388 for EnsureMember<AccountId, I>
1389where
1390 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1391{
1392 type Success = AccountId;
1393 fn try_origin(o: O) -> Result<Self::Success, O> {
1394 match o.caller().try_into() {
1395 Ok(RawOrigin::Member(id)) => return Ok(id.clone()),
1396 _ => (),
1397 }
1398
1399 Err(o)
1400 }
1401
1402 #[cfg(feature = "runtime-benchmarks")]
1403 fn try_successful_origin() -> Result<O, ()> {
1404 let zero_account_id =
1405 AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
1406 .expect("infinite length input; no invalid inputs for type; qed");
1407 Ok(O::from(RawOrigin::Member(zero_account_id)))
1408 }
1409}
1410
1411impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone, T>
1412 EnsureOriginWithArg<O, T> for EnsureMember<AccountId, I>
1413where
1414 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1415{
1416 type Success = <Self as EnsureOrigin<O>>::Success;
1417 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1418 <Self as EnsureOrigin<O>>::try_origin(o)
1419 }
1420 #[cfg(feature = "runtime-benchmarks")]
1421 fn try_successful_origin(_: &T) -> Result<O, ()> {
1422 <Self as EnsureOrigin<O>>::try_successful_origin()
1423 }
1424}
1425
1426pub struct EnsureMembers<AccountId, I: 'static, const N: u32>(PhantomData<(AccountId, I)>);
1427impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32> EnsureOrigin<O>
1428 for EnsureMembers<AccountId, I, N>
1429where
1430 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1431{
1432 type Success = (MemberCount, MemberCount);
1433 fn try_origin(o: O) -> Result<Self::Success, O> {
1434 match o.caller().try_into() {
1435 Ok(RawOrigin::Members(n, m)) if *n >= N => return Ok((*n, *m)),
1436 _ => (),
1437 }
1438
1439 Err(o)
1440 }
1441
1442 #[cfg(feature = "runtime-benchmarks")]
1443 fn try_successful_origin() -> Result<O, ()> {
1444 Ok(O::from(RawOrigin::Members(N, N)))
1445 }
1446}
1447
1448impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, T>
1449 EnsureOriginWithArg<O, T> for EnsureMembers<AccountId, I, N>
1450where
1451 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1452{
1453 type Success = <Self as EnsureOrigin<O>>::Success;
1454 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1455 <Self as EnsureOrigin<O>>::try_origin(o)
1456 }
1457 #[cfg(feature = "runtime-benchmarks")]
1458 fn try_successful_origin(_: &T) -> Result<O, ()> {
1459 <Self as EnsureOrigin<O>>::try_successful_origin()
1460 }
1461}
1462
1463pub struct EnsureProportionMoreThan<AccountId, I: 'static, const N: u32, const D: u32>(
1464 PhantomData<(AccountId, I)>,
1465);
1466impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1467 EnsureOrigin<O> for EnsureProportionMoreThan<AccountId, I, N, D>
1468where
1469 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1470{
1471 type Success = ();
1472 fn try_origin(o: O) -> Result<Self::Success, O> {
1473 match o.caller().try_into() {
1474 Ok(RawOrigin::Members(n, m)) if n * D > N * m => return Ok(()),
1475 _ => (),
1476 }
1477
1478 Err(o)
1479 }
1480
1481 #[cfg(feature = "runtime-benchmarks")]
1482 fn try_successful_origin() -> Result<O, ()> {
1483 Ok(O::from(RawOrigin::Members(1u32, 0u32)))
1484 }
1485}
1486
1487impl<
1488 O: OriginTrait + From<RawOrigin<AccountId, I>>,
1489 AccountId,
1490 I,
1491 const N: u32,
1492 const D: u32,
1493 T,
1494 > EnsureOriginWithArg<O, T> for EnsureProportionMoreThan<AccountId, I, N, D>
1495where
1496 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1497{
1498 type Success = <Self as EnsureOrigin<O>>::Success;
1499 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1500 <Self as EnsureOrigin<O>>::try_origin(o)
1501 }
1502 #[cfg(feature = "runtime-benchmarks")]
1503 fn try_successful_origin(_: &T) -> Result<O, ()> {
1504 <Self as EnsureOrigin<O>>::try_successful_origin()
1505 }
1506}
1507
1508pub struct EnsureProportionAtLeast<AccountId, I: 'static, const N: u32, const D: u32>(
1509 PhantomData<(AccountId, I)>,
1510);
1511impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1512 EnsureOrigin<O> for EnsureProportionAtLeast<AccountId, I, N, D>
1513where
1514 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1515{
1516 type Success = ();
1517 fn try_origin(o: O) -> Result<Self::Success, O> {
1518 match o.caller().try_into() {
1519 Ok(RawOrigin::Members(n, m)) if n * D >= N * m => return Ok(()),
1520 _ => (),
1521 };
1522
1523 Err(o)
1524 }
1525
1526 #[cfg(feature = "runtime-benchmarks")]
1527 fn try_successful_origin() -> Result<O, ()> {
1528 Ok(O::from(RawOrigin::Members(0u32, 0u32)))
1529 }
1530}
1531
1532impl<
1533 O: OriginTrait + From<RawOrigin<AccountId, I>>,
1534 AccountId,
1535 I,
1536 const N: u32,
1537 const D: u32,
1538 T,
1539 > EnsureOriginWithArg<O, T> for EnsureProportionAtLeast<AccountId, I, N, D>
1540where
1541 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1542{
1543 type Success = <Self as EnsureOrigin<O>>::Success;
1544 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1545 <Self as EnsureOrigin<O>>::try_origin(o)
1546 }
1547 #[cfg(feature = "runtime-benchmarks")]
1548 fn try_successful_origin(_: &T) -> Result<O, ()> {
1549 <Self as EnsureOrigin<O>>::try_successful_origin()
1550 }
1551}