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 DispatchError, RuntimeDebug,
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,
142 Eq,
143 Clone,
144 RuntimeDebug,
145 Encode,
146 Decode,
147 DecodeWithMemTracking,
148 TypeInfo,
149 MaxEncodedLen,
150)]
151#[scale_info(skip_type_params(I))]
152#[codec(mel_bound(AccountId: MaxEncodedLen))]
153pub enum RawOrigin<AccountId, I> {
154 Members(MemberCount, MemberCount),
156 Member(AccountId),
158 _Phantom(PhantomData<I>),
160}
161
162impl<AccountId, I> GetBacking for RawOrigin<AccountId, I> {
163 fn get_backing(&self) -> Option<Backing> {
164 match self {
165 RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }),
166 _ => None,
167 }
168 }
169}
170
171#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
173pub struct Votes<AccountId, BlockNumber> {
174 index: ProposalIndex,
176 threshold: MemberCount,
178 ayes: Vec<AccountId>,
180 nays: Vec<AccountId>,
182 end: BlockNumber,
184}
185
186#[doc = docify::embed!("src/tests.rs", deposit_types_with_linear_work)]
196#[doc = docify::embed!("src/tests.rs", deposit_types_with_geometric_work)]
199#[doc = docify::embed!("src/tests.rs", deposit_round_with_geometric_work)]
202pub mod deposit {
203 use core::marker::PhantomData;
204 use sp_core::Get;
205 use sp_runtime::{traits::Convert, FixedPointNumber, FixedU128, Saturating};
206
207 pub struct Constant<Deposit>(PhantomData<Deposit>);
210 impl<Deposit, Balance> Convert<u32, Balance> for Constant<Deposit>
211 where
212 Deposit: Get<Balance>,
213 Balance: frame_support::traits::tokens::Balance,
214 {
215 fn convert(_: u32) -> Balance {
216 Deposit::get()
217 }
218 }
219
220 pub struct Linear<Slope, Offset>(PhantomData<(Slope, Offset)>);
223 impl<Slope, Offset, Balance> Convert<u32, Balance> for Linear<Slope, Offset>
224 where
225 Slope: Get<u32>,
226 Offset: Get<Balance>,
227 Balance: frame_support::traits::tokens::Balance,
228 {
229 fn convert(proposal_count: u32) -> Balance {
230 let base: Balance = Slope::get().saturating_mul(proposal_count).into();
231 Offset::get().saturating_add(base)
232 }
233 }
234
235 pub struct Geometric<Ratio, Base>(PhantomData<(Ratio, Base)>);
238 impl<Ratio, Base, Balance> Convert<u32, Balance> for Geometric<Ratio, Base>
239 where
240 Ratio: Get<FixedU128>,
241 Base: Get<Balance>,
242 Balance: frame_support::traits::tokens::Balance,
243 {
244 fn convert(proposal_count: u32) -> Balance {
245 Ratio::get()
246 .saturating_pow(proposal_count as usize)
247 .saturating_mul_int(Base::get())
248 }
249 }
250
251 pub struct Round<Precision, Deposit>(PhantomData<(Precision, Deposit)>);
255 impl<Precision, Deposit, Balance> Convert<u32, Balance> for Round<Precision, Deposit>
256 where
257 Precision: Get<u32>,
258 Deposit: Convert<u32, Balance>,
259 Balance: frame_support::traits::tokens::Balance,
260 {
261 fn convert(proposal_count: u32) -> Balance {
262 let deposit = Deposit::convert(proposal_count);
263 if !deposit.is_zero() {
264 let factor: Balance =
265 Balance::from(10u32).saturating_pow(Precision::get() as usize);
266 if factor > deposit {
267 deposit
268 } else {
269 (deposit / factor) * factor
270 }
271 } else {
272 deposit
273 }
274 }
275 }
276
277 pub struct Stepped<Period, Step>(PhantomData<(Period, Step)>);
279 impl<Period, Step, Balance> Convert<u32, Balance> for Stepped<Period, Step>
280 where
281 Period: Get<u32>,
282 Step: Convert<u32, Balance>,
283 Balance: frame_support::traits::tokens::Balance,
284 {
285 fn convert(proposal_count: u32) -> Balance {
286 let step_num = proposal_count / Period::get();
287 Step::convert(step_num)
288 }
289 }
290
291 pub struct Delayed<Delay, Deposit>(PhantomData<(Delay, Deposit)>);
293 impl<Delay, Deposit, Balance> Convert<u32, Balance> for Delayed<Delay, Deposit>
294 where
295 Delay: Get<u32>,
296 Deposit: Convert<u32, Balance>,
297 Balance: frame_support::traits::tokens::Balance,
298 {
299 fn convert(proposal_count: u32) -> Balance {
300 let delay = Delay::get();
301 if delay > proposal_count {
302 return Balance::zero();
303 }
304 let pos = proposal_count.saturating_sub(delay);
305 Deposit::convert(pos)
306 }
307 }
308
309 pub struct WithCeil<Ceil, Deposit>(PhantomData<(Ceil, Deposit)>);
311 impl<Ceil, Deposit, Balance> Convert<u32, Balance> for WithCeil<Ceil, Deposit>
312 where
313 Ceil: Get<Balance>,
314 Deposit: Convert<u32, Balance>,
315 Balance: frame_support::traits::tokens::Balance,
316 {
317 fn convert(proposal_count: u32) -> Balance {
318 Deposit::convert(proposal_count).min(Ceil::get())
319 }
320 }
321}
322
323#[frame_support::pallet]
324pub mod pallet {
325 use super::*;
326 use frame_support::pallet_prelude::*;
327 use frame_system::pallet_prelude::*;
328
329 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
331
332 #[pallet::pallet]
333 #[pallet::storage_version(STORAGE_VERSION)]
334 #[pallet::without_storage_info]
335 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
336
337 #[pallet::config]
338 pub trait Config<I: 'static = ()>: frame_system::Config {
339 type RuntimeOrigin: From<RawOrigin<Self::AccountId, I>>;
341
342 type Proposal: Parameter
344 + Dispatchable<
345 RuntimeOrigin = <Self as Config<I>>::RuntimeOrigin,
346 PostInfo = PostDispatchInfo,
347 > + From<frame_system::Call<Self>>
348 + GetDispatchInfo;
349
350 #[allow(deprecated)]
352 type RuntimeEvent: From<Event<Self, I>>
353 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
354
355 type MotionDuration: Get<BlockNumberFor<Self>>;
357
358 type MaxProposals: Get<ProposalIndex>;
360
361 type MaxMembers: Get<MemberCount>;
367
368 type DefaultVote: DefaultVote;
370
371 type WeightInfo: WeightInfo;
373
374 type SetMembersOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
376
377 #[pallet::constant]
379 type MaxProposalWeight: Get<Weight>;
380
381 type DisapproveOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
384
385 type KillOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
390
391 type Consideration: MaybeConsideration<Self::AccountId, u32>;
401 }
402
403 #[pallet::genesis_config]
404 #[derive(frame_support::DefaultNoBound)]
405 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
406 #[serde(skip)]
407 pub phantom: PhantomData<I>,
408 pub members: Vec<T::AccountId>,
409 }
410
411 #[pallet::genesis_build]
412 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
413 fn build(&self) {
414 use alloc::collections::btree_set::BTreeSet;
415 let members_set: BTreeSet<_> = self.members.iter().collect();
416 assert_eq!(
417 members_set.len(),
418 self.members.len(),
419 "Members cannot contain duplicate accounts."
420 );
421 assert!(
422 self.members.len() <= T::MaxMembers::get() as usize,
423 "Members length cannot exceed MaxMembers.",
424 );
425
426 Pallet::<T, I>::initialize_members(&self.members)
427 }
428 }
429
430 #[pallet::origin]
432 pub type Origin<T, I = ()> = RawOrigin<<T as frame_system::Config>::AccountId, I>;
433
434 #[pallet::storage]
436 pub type Proposals<T: Config<I>, I: 'static = ()> =
437 StorageValue<_, BoundedVec<T::Hash, T::MaxProposals>, ValueQuery>;
438
439 #[pallet::storage]
441 pub type ProposalOf<T: Config<I>, I: 'static = ()> =
442 StorageMap<_, Identity, T::Hash, <T as Config<I>>::Proposal, OptionQuery>;
443
444 #[pallet::storage]
449 pub type CostOf<T: Config<I>, I: 'static = ()> =
450 StorageMap<_, Identity, T::Hash, (T::AccountId, T::Consideration), OptionQuery>;
451
452 #[pallet::storage]
454 pub type Voting<T: Config<I>, I: 'static = ()> =
455 StorageMap<_, Identity, T::Hash, Votes<T::AccountId, BlockNumberFor<T>>, OptionQuery>;
456
457 #[pallet::storage]
459 pub type ProposalCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
460
461 #[pallet::storage]
463 pub type Members<T: Config<I>, I: 'static = ()> =
464 StorageValue<_, Vec<T::AccountId>, ValueQuery>;
465
466 #[pallet::storage]
468 pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
469
470 #[pallet::event]
471 #[pallet::generate_deposit(pub(super) fn deposit_event)]
472 pub enum Event<T: Config<I>, I: 'static = ()> {
473 Proposed {
476 account: T::AccountId,
477 proposal_index: ProposalIndex,
478 proposal_hash: T::Hash,
479 threshold: MemberCount,
480 },
481 Voted {
484 account: T::AccountId,
485 proposal_hash: T::Hash,
486 voted: bool,
487 yes: MemberCount,
488 no: MemberCount,
489 },
490 Approved { proposal_hash: T::Hash },
492 Disapproved { proposal_hash: T::Hash },
494 Executed { proposal_hash: T::Hash, result: DispatchResult },
496 MemberExecuted { proposal_hash: T::Hash, result: DispatchResult },
498 Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount },
500 Killed { proposal_hash: T::Hash },
502 ProposalCostBurned { proposal_hash: T::Hash, who: T::AccountId },
504 ProposalCostReleased { proposal_hash: T::Hash, who: T::AccountId },
506 }
507
508 #[pallet::error]
509 pub enum Error<T, I = ()> {
510 NotMember,
512 DuplicateProposal,
514 ProposalMissing,
516 WrongIndex,
518 DuplicateVote,
520 AlreadyInitialized,
522 TooEarly,
524 TooManyProposals,
526 WrongProposalWeight,
528 WrongProposalLength,
530 PrimeAccountNotMember,
532 ProposalActive,
534 }
535
536 #[pallet::hooks]
537 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
538 #[cfg(feature = "try-runtime")]
539 fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
540 Self::do_try_state()
541 }
542 }
543
544 #[pallet::composite_enum]
546 pub enum HoldReason<I: 'static = ()> {
547 #[codec(index = 0)]
549 ProposalSubmission,
550 }
551
552 #[pallet::call]
554 impl<T: Config<I>, I: 'static> Pallet<T, I> {
555 #[pallet::call_index(0)]
580 #[pallet::weight((
581 T::WeightInfo::set_members(
582 *old_count, new_members.len() as u32, T::MaxProposals::get() ),
586 DispatchClass::Operational
587 ))]
588 pub fn set_members(
589 origin: OriginFor<T>,
590 new_members: Vec<T::AccountId>,
591 prime: Option<T::AccountId>,
592 old_count: MemberCount,
593 ) -> DispatchResultWithPostInfo {
594 T::SetMembersOrigin::ensure_origin(origin)?;
595 if new_members.len() > T::MaxMembers::get() as usize {
596 log::error!(
597 target: LOG_TARGET,
598 "New members count ({}) exceeds maximum amount of members expected ({}).",
599 new_members.len(),
600 T::MaxMembers::get(),
601 );
602 }
603
604 let old = Members::<T, I>::get();
605 if old.len() > old_count as usize {
606 log::warn!(
607 target: LOG_TARGET,
608 "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})",
609 old_count,
610 old.len(),
611 );
612 }
613 if let Some(p) = &prime {
614 ensure!(new_members.contains(p), Error::<T, I>::PrimeAccountNotMember);
615 }
616 let mut new_members = new_members;
617 new_members.sort();
618 <Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members, &old);
619 Prime::<T, I>::set(prime);
620
621 Ok(Some(T::WeightInfo::set_members(
622 old.len() as u32, new_members.len() as u32, T::MaxProposals::get(), ))
626 .into())
627 }
628
629 #[pallet::call_index(1)]
639 #[pallet::weight((
640 T::WeightInfo::execute(
641 *length_bound, T::MaxMembers::get(), ).saturating_add(proposal.get_dispatch_info().call_weight), DispatchClass::Operational
645 ))]
646 pub fn execute(
647 origin: OriginFor<T>,
648 proposal: Box<<T as Config<I>>::Proposal>,
649 #[pallet::compact] length_bound: u32,
650 ) -> DispatchResultWithPostInfo {
651 let who = ensure_signed(origin)?;
652 let members = Members::<T, I>::get();
653 ensure!(members.contains(&who), Error::<T, I>::NotMember);
654 let proposal_len = proposal.encoded_size();
655 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
656
657 let proposal_hash = T::Hashing::hash_of(&proposal);
658 let result = proposal.dispatch(RawOrigin::Member(who).into());
659 Self::deposit_event(Event::MemberExecuted {
660 proposal_hash,
661 result: result.map(|_| ()).map_err(|e| e.error),
662 });
663
664 Ok(get_result_weight(result)
665 .map(|w| {
666 T::WeightInfo::execute(
667 proposal_len as u32, members.len() as u32, )
670 .saturating_add(w) })
672 .into())
673 }
674
675 #[pallet::call_index(2)]
690 #[pallet::weight((
691 if *threshold < 2 {
692 T::WeightInfo::propose_execute(
693 *length_bound, T::MaxMembers::get(), ).saturating_add(proposal.get_dispatch_info().call_weight) } else {
697 T::WeightInfo::propose_proposed(
698 *length_bound, T::MaxMembers::get(), T::MaxProposals::get(), )
702 },
703 DispatchClass::Operational
704 ))]
705 pub fn propose(
706 origin: OriginFor<T>,
707 #[pallet::compact] threshold: MemberCount,
708 proposal: Box<<T as Config<I>>::Proposal>,
709 #[pallet::compact] length_bound: u32,
710 ) -> DispatchResultWithPostInfo {
711 let who = ensure_signed(origin)?;
712 let members = Members::<T, I>::get();
713 ensure!(members.contains(&who), Error::<T, I>::NotMember);
714
715 if threshold < 2 {
716 let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?;
717
718 Ok(get_result_weight(result)
719 .map(|w| {
720 T::WeightInfo::propose_execute(
721 proposal_len as u32, members.len() as u32, )
724 .saturating_add(w) })
726 .into())
727 } else {
728 let (proposal_len, active_proposals) =
729 Self::do_propose_proposed(who, threshold, proposal, length_bound)?;
730
731 Ok(Some(T::WeightInfo::propose_proposed(
732 proposal_len as u32, members.len() as u32, active_proposals, ))
736 .into())
737 }
738 }
739
740 #[pallet::call_index(3)]
750 #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))]
751 pub fn vote(
752 origin: OriginFor<T>,
753 proposal: T::Hash,
754 #[pallet::compact] index: ProposalIndex,
755 approve: bool,
756 ) -> DispatchResultWithPostInfo {
757 let who = ensure_signed(origin)?;
758 let members = Members::<T, I>::get();
759 ensure!(members.contains(&who), Error::<T, I>::NotMember);
760
761 let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?;
763
764 if is_account_voting_first_time {
765 Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into())
766 } else {
767 Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into())
768 }
769 }
770
771 #[pallet::call_index(5)]
784 #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))]
785 pub fn disapprove_proposal(
786 origin: OriginFor<T>,
787 proposal_hash: T::Hash,
788 ) -> DispatchResultWithPostInfo {
789 T::DisapproveOrigin::ensure_origin(origin)?;
790 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
791 Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into())
792 }
793
794 #[pallet::call_index(6)]
819 #[pallet::weight((
820 {
821 let b = *length_bound;
822 let m = T::MaxMembers::get();
823 let p1 = *proposal_weight_bound;
824 let p2 = T::MaxProposals::get();
825 T::WeightInfo::close_early_approved(b, m, p2)
826 .max(T::WeightInfo::close_early_disapproved(m, p2))
827 .max(T::WeightInfo::close_approved(b, m, p2))
828 .max(T::WeightInfo::close_disapproved(m, p2))
829 .saturating_add(p1)
830 },
831 DispatchClass::Operational
832 ))]
833 pub fn close(
834 origin: OriginFor<T>,
835 proposal_hash: T::Hash,
836 #[pallet::compact] index: ProposalIndex,
837 proposal_weight_bound: Weight,
838 #[pallet::compact] length_bound: u32,
839 ) -> DispatchResultWithPostInfo {
840 ensure_signed(origin)?;
841
842 Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
843 }
844
845 #[pallet::call_index(7)]
853 #[pallet::weight(T::WeightInfo::kill(1, T::MaxProposals::get()))]
854 pub fn kill(origin: OriginFor<T>, proposal_hash: T::Hash) -> DispatchResultWithPostInfo {
855 T::KillOrigin::ensure_origin(origin)?;
856 ensure!(
857 ProposalOf::<T, I>::get(&proposal_hash).is_some(),
858 Error::<T, I>::ProposalMissing
859 );
860 let burned = if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
861 cost.burn(&who);
862 Self::deposit_event(Event::ProposalCostBurned { proposal_hash, who });
863 true
864 } else {
865 false
866 };
867 let proposal_count = Self::remove_proposal(proposal_hash);
868
869 Self::deposit_event(Event::Killed { proposal_hash });
870
871 Ok(Some(T::WeightInfo::kill(burned as u32, proposal_count)).into())
872 }
873
874 #[pallet::call_index(8)]
884 #[pallet::weight(T::WeightInfo::release_proposal_cost())]
885 pub fn release_proposal_cost(
886 origin: OriginFor<T>,
887 proposal_hash: T::Hash,
888 ) -> DispatchResult {
889 ensure_signed_or_root(origin)?;
890 ensure!(
891 ProposalOf::<T, I>::get(&proposal_hash).is_none(),
892 Error::<T, I>::ProposalActive
893 );
894 if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
895 cost.drop(&who)?;
896 Self::deposit_event(Event::ProposalCostReleased { proposal_hash, who });
897 }
898
899 Ok(())
900 }
901 }
902}
903
904fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
908 match result {
909 Ok(post_info) => post_info.actual_weight,
910 Err(err) => err.post_info.actual_weight,
911 }
912}
913
914impl<T: Config<I>, I: 'static> Pallet<T, I> {
915 pub fn is_member(who: &T::AccountId) -> bool {
917 Members::<T, I>::get().contains(who)
920 }
921
922 pub fn do_propose_execute(
924 proposal: Box<<T as Config<I>>::Proposal>,
925 length_bound: MemberCount,
926 ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> {
927 let proposal_len = proposal.encoded_size();
928 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
929 let proposal_weight = proposal.get_dispatch_info().call_weight;
930 ensure!(
931 proposal_weight.all_lte(T::MaxProposalWeight::get()),
932 Error::<T, I>::WrongProposalWeight
933 );
934
935 let proposal_hash = T::Hashing::hash_of(&proposal);
936 ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
937
938 let seats = Members::<T, I>::get().len() as MemberCount;
939 let result = proposal.dispatch(RawOrigin::Members(1, seats).into());
940 Self::deposit_event(Event::Executed {
941 proposal_hash,
942 result: result.map(|_| ()).map_err(|e| e.error),
943 });
944 Ok((proposal_len as u32, result))
945 }
946
947 pub fn do_propose_proposed(
949 who: T::AccountId,
950 threshold: MemberCount,
951 proposal: Box<<T as Config<I>>::Proposal>,
952 length_bound: MemberCount,
953 ) -> Result<(u32, u32), DispatchError> {
954 let proposal_len = proposal.encoded_size();
955 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
956 let proposal_weight = proposal.get_dispatch_info().call_weight;
957 ensure!(
958 proposal_weight.all_lte(T::MaxProposalWeight::get()),
959 Error::<T, I>::WrongProposalWeight
960 );
961
962 let proposal_hash = T::Hashing::hash_of(&proposal);
963 ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
964
965 let active_proposals =
966 <Proposals<T, I>>::try_mutate(|proposals| -> Result<usize, DispatchError> {
967 proposals.try_push(proposal_hash).map_err(|_| Error::<T, I>::TooManyProposals)?;
968 Ok(proposals.len())
969 })?;
970
971 let cost = T::Consideration::new(&who, active_proposals as u32 - 1)?;
972 if !cost.is_none() {
973 <CostOf<T, I>>::insert(proposal_hash, (who.clone(), cost));
974 }
975
976 let index = ProposalCount::<T, I>::get();
977
978 <ProposalCount<T, I>>::mutate(|i| *i += 1);
979 <ProposalOf<T, I>>::insert(proposal_hash, proposal);
980 let votes = {
981 let end = frame_system::Pallet::<T>::block_number() + T::MotionDuration::get();
982 Votes { index, threshold, ayes: vec![], nays: vec![], end }
983 };
984 <Voting<T, I>>::insert(proposal_hash, votes);
985
986 Self::deposit_event(Event::Proposed {
987 account: who,
988 proposal_index: index,
989 proposal_hash,
990 threshold,
991 });
992 Ok((proposal_len as u32, active_proposals as u32))
993 }
994
995 pub fn do_vote(
998 who: T::AccountId,
999 proposal: T::Hash,
1000 index: ProposalIndex,
1001 approve: bool,
1002 ) -> Result<bool, DispatchError> {
1003 let mut voting = Voting::<T, I>::get(&proposal).ok_or(Error::<T, I>::ProposalMissing)?;
1004 ensure!(voting.index == index, Error::<T, I>::WrongIndex);
1005
1006 let position_yes = voting.ayes.iter().position(|a| a == &who);
1007 let position_no = voting.nays.iter().position(|a| a == &who);
1008
1009 let is_account_voting_first_time = position_yes.is_none() && position_no.is_none();
1011
1012 if approve {
1013 if position_yes.is_none() {
1014 voting.ayes.push(who.clone());
1015 } else {
1016 return Err(Error::<T, I>::DuplicateVote.into())
1017 }
1018 if let Some(pos) = position_no {
1019 voting.nays.swap_remove(pos);
1020 }
1021 } else {
1022 if position_no.is_none() {
1023 voting.nays.push(who.clone());
1024 } else {
1025 return Err(Error::<T, I>::DuplicateVote.into())
1026 }
1027 if let Some(pos) = position_yes {
1028 voting.ayes.swap_remove(pos);
1029 }
1030 }
1031
1032 let yes_votes = voting.ayes.len() as MemberCount;
1033 let no_votes = voting.nays.len() as MemberCount;
1034 Self::deposit_event(Event::Voted {
1035 account: who,
1036 proposal_hash: proposal,
1037 voted: approve,
1038 yes: yes_votes,
1039 no: no_votes,
1040 });
1041
1042 Voting::<T, I>::insert(&proposal, voting);
1043
1044 Ok(is_account_voting_first_time)
1045 }
1046
1047 pub fn do_close(
1049 proposal_hash: T::Hash,
1050 index: ProposalIndex,
1051 proposal_weight_bound: Weight,
1052 length_bound: u32,
1053 ) -> DispatchResultWithPostInfo {
1054 let voting = Voting::<T, I>::get(&proposal_hash).ok_or(Error::<T, I>::ProposalMissing)?;
1055 ensure!(voting.index == index, Error::<T, I>::WrongIndex);
1056
1057 let mut no_votes = voting.nays.len() as MemberCount;
1058 let mut yes_votes = voting.ayes.len() as MemberCount;
1059 let seats = Members::<T, I>::get().len() as MemberCount;
1060 let approved = yes_votes >= voting.threshold;
1061 let disapproved = seats.saturating_sub(no_votes) < voting.threshold;
1062 if approved {
1064 let (proposal, len) = Self::validate_and_get_proposal(
1065 &proposal_hash,
1066 length_bound,
1067 proposal_weight_bound,
1068 )?;
1069 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1070 let (proposal_weight, proposal_count) =
1071 Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1072 return Ok((
1073 Some(
1074 T::WeightInfo::close_early_approved(len as u32, seats, proposal_count)
1075 .saturating_add(proposal_weight),
1076 ),
1077 Pays::Yes,
1078 )
1079 .into())
1080 } else if disapproved {
1081 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1082 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1083 return Ok((
1084 Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)),
1085 Pays::No,
1086 )
1087 .into())
1088 }
1089
1090 ensure!(frame_system::Pallet::<T>::block_number() >= voting.end, Error::<T, I>::TooEarly);
1092
1093 let prime_vote = Prime::<T, I>::get().map(|who| voting.ayes.iter().any(|a| a == &who));
1094
1095 let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats);
1097
1098 let abstentions = seats - (yes_votes + no_votes);
1099 match default {
1100 true => yes_votes += abstentions,
1101 false => no_votes += abstentions,
1102 }
1103 let approved = yes_votes >= voting.threshold;
1104
1105 if approved {
1106 let (proposal, len) = Self::validate_and_get_proposal(
1107 &proposal_hash,
1108 length_bound,
1109 proposal_weight_bound,
1110 )?;
1111 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1112 let (proposal_weight, proposal_count) =
1113 Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1114 Ok((
1115 Some(
1116 T::WeightInfo::close_approved(len as u32, seats, proposal_count)
1117 .saturating_add(proposal_weight),
1118 ),
1119 Pays::Yes,
1120 )
1121 .into())
1122 } else {
1123 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1124 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1125 Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into())
1126 }
1127 }
1128
1129 fn validate_and_get_proposal(
1134 hash: &T::Hash,
1135 length_bound: u32,
1136 weight_bound: Weight,
1137 ) -> Result<(<T as Config<I>>::Proposal, usize), DispatchError> {
1138 let key = ProposalOf::<T, I>::hashed_key_for(hash);
1139 let proposal_len =
1141 storage::read(&key, &mut [0; 0], 0).ok_or(Error::<T, I>::ProposalMissing)?;
1142 ensure!(proposal_len <= length_bound, Error::<T, I>::WrongProposalLength);
1143 let proposal = ProposalOf::<T, I>::get(hash).ok_or(Error::<T, I>::ProposalMissing)?;
1144 let proposal_weight = proposal.get_dispatch_info().call_weight;
1145 ensure!(proposal_weight.all_lte(weight_bound), Error::<T, I>::WrongProposalWeight);
1146 Ok((proposal, proposal_len as usize))
1147 }
1148
1149 fn do_approve_proposal(
1164 seats: MemberCount,
1165 yes_votes: MemberCount,
1166 proposal_hash: T::Hash,
1167 proposal: <T as Config<I>>::Proposal,
1168 ) -> (Weight, u32) {
1169 Self::deposit_event(Event::Approved { proposal_hash });
1170
1171 let dispatch_weight = proposal.get_dispatch_info().call_weight;
1172 let origin = RawOrigin::Members(yes_votes, seats).into();
1173 let result = proposal.dispatch(origin);
1174 Self::deposit_event(Event::Executed {
1175 proposal_hash,
1176 result: result.map(|_| ()).map_err(|e| e.error),
1177 });
1178 let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); let proposal_count = Self::remove_proposal(proposal_hash);
1182 (proposal_weight, proposal_count)
1183 }
1184
1185 pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 {
1187 Self::deposit_event(Event::Disapproved { proposal_hash });
1189 Self::remove_proposal(proposal_hash)
1190 }
1191
1192 fn remove_proposal(proposal_hash: T::Hash) -> u32 {
1194 ProposalOf::<T, I>::remove(&proposal_hash);
1196 Voting::<T, I>::remove(&proposal_hash);
1197 let num_proposals = Proposals::<T, I>::mutate(|proposals| {
1198 proposals.retain(|h| h != &proposal_hash);
1199 proposals.len() + 1 });
1201 num_proposals as u32
1202 }
1203
1204 #[cfg(any(feature = "try-runtime", test))]
1233 fn do_try_state() -> Result<(), TryRuntimeError> {
1234 Proposals::<T, I>::get().into_iter().try_for_each(
1235 |proposal| -> Result<(), TryRuntimeError> {
1236 ensure!(
1237 ProposalOf::<T, I>::get(proposal).is_some(),
1238 "Proposal hash from `Proposals` is not found inside the `ProposalOf` mapping."
1239 );
1240 Ok(())
1241 },
1242 )?;
1243
1244 ensure!(
1245 Proposals::<T, I>::get().into_iter().count() <= ProposalCount::<T, I>::get() as usize,
1246 "The actual number of proposals is greater than `ProposalCount`"
1247 );
1248 ensure!(
1249 Proposals::<T, I>::get().into_iter().count() == <ProposalOf<T, I>>::iter_keys().count(),
1250 "Proposal count inside `Proposals` is not equal to the proposal count in `ProposalOf`"
1251 );
1252
1253 Proposals::<T, I>::get().into_iter().try_for_each(
1254 |proposal| -> Result<(), TryRuntimeError> {
1255 if let Some(votes) = Voting::<T, I>::get(proposal) {
1256 let ayes = votes.ayes.len();
1257 let nays = votes.nays.len();
1258
1259 ensure!(
1260 ayes.saturating_add(nays) <= T::MaxMembers::get() as usize,
1261 "The sum of ayes and nays is greater than `MaxMembers`"
1262 );
1263 }
1264 Ok(())
1265 },
1266 )?;
1267
1268 let mut proposal_indices = vec![];
1269 Proposals::<T, I>::get().into_iter().try_for_each(
1270 |proposal| -> Result<(), TryRuntimeError> {
1271 if let Some(votes) = Voting::<T, I>::get(proposal) {
1272 let proposal_index = votes.index;
1273 ensure!(
1274 !proposal_indices.contains(&proposal_index),
1275 "The proposal index is not unique."
1276 );
1277 proposal_indices.push(proposal_index);
1278 }
1279 Ok(())
1280 },
1281 )?;
1282
1283 <Voting<T, I>>::iter_keys().try_for_each(
1284 |proposal_hash| -> Result<(), TryRuntimeError> {
1285 ensure!(
1286 Proposals::<T, I>::get().contains(&proposal_hash),
1287 "`Proposals` doesn't contain the proposal hash from the `Voting` storage map."
1288 );
1289 Ok(())
1290 },
1291 )?;
1292
1293 ensure!(
1294 Members::<T, I>::get().len() <= T::MaxMembers::get() as usize,
1295 "The member count is greater than `MaxMembers`."
1296 );
1297
1298 ensure!(
1299 Members::<T, I>::get().windows(2).all(|members| members[0] <= members[1]),
1300 "The members are not sorted by value."
1301 );
1302
1303 if let Some(prime) = Prime::<T, I>::get() {
1304 ensure!(Members::<T, I>::get().contains(&prime), "Prime account is not a member.");
1305 }
1306
1307 Ok(())
1308 }
1309}
1310
1311impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> {
1312 fn change_members_sorted(
1323 _incoming: &[T::AccountId],
1324 outgoing: &[T::AccountId],
1325 new: &[T::AccountId],
1326 ) {
1327 if new.len() > T::MaxMembers::get() as usize {
1328 log::error!(
1329 target: LOG_TARGET,
1330 "New members count ({}) exceeds maximum amount of members expected ({}).",
1331 new.len(),
1332 T::MaxMembers::get(),
1333 );
1334 }
1335 let mut outgoing = outgoing.to_vec();
1337 outgoing.sort();
1338 for h in Proposals::<T, I>::get().into_iter() {
1339 <Voting<T, I>>::mutate(h, |v| {
1340 if let Some(mut votes) = v.take() {
1341 votes.ayes = votes
1342 .ayes
1343 .into_iter()
1344 .filter(|i| outgoing.binary_search(i).is_err())
1345 .collect();
1346 votes.nays = votes
1347 .nays
1348 .into_iter()
1349 .filter(|i| outgoing.binary_search(i).is_err())
1350 .collect();
1351 *v = Some(votes);
1352 }
1353 });
1354 }
1355 Members::<T, I>::put(new);
1356 Prime::<T, I>::kill();
1357 }
1358
1359 fn set_prime(prime: Option<T::AccountId>) {
1360 Prime::<T, I>::set(prime);
1361 }
1362
1363 fn get_prime() -> Option<T::AccountId> {
1364 Prime::<T, I>::get()
1365 }
1366}
1367
1368impl<T: Config<I>, I: 'static> InitializeMembers<T::AccountId> for Pallet<T, I> {
1369 fn initialize_members(members: &[T::AccountId]) {
1370 if !members.is_empty() {
1371 assert!(<Members<T, I>>::get().is_empty(), "Members are already initialized!");
1372 let mut members = members.to_vec();
1373 members.sort();
1374 <Members<T, I>>::put(members);
1375 }
1376 }
1377}
1378
1379pub fn ensure_members<OuterOrigin, AccountId, I>(
1382 o: OuterOrigin,
1383 n: MemberCount,
1384) -> result::Result<MemberCount, &'static str>
1385where
1386 OuterOrigin: Into<result::Result<RawOrigin<AccountId, I>, OuterOrigin>>,
1387{
1388 match o.into() {
1389 Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n),
1390 _ => Err("bad origin: expected to be a threshold number of members"),
1391 }
1392}
1393
1394pub struct EnsureMember<AccountId, I: 'static>(PhantomData<(AccountId, I)>);
1395impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone> EnsureOrigin<O>
1396 for EnsureMember<AccountId, I>
1397where
1398 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1399{
1400 type Success = AccountId;
1401 fn try_origin(o: O) -> Result<Self::Success, O> {
1402 match o.caller().try_into() {
1403 Ok(RawOrigin::Member(id)) => return Ok(id.clone()),
1404 _ => (),
1405 }
1406
1407 Err(o)
1408 }
1409
1410 #[cfg(feature = "runtime-benchmarks")]
1411 fn try_successful_origin() -> Result<O, ()> {
1412 let zero_account_id =
1413 AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
1414 .expect("infinite length input; no invalid inputs for type; qed");
1415 Ok(O::from(RawOrigin::Member(zero_account_id)))
1416 }
1417}
1418
1419impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone, T>
1420 EnsureOriginWithArg<O, T> for EnsureMember<AccountId, I>
1421where
1422 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1423{
1424 type Success = <Self as EnsureOrigin<O>>::Success;
1425 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1426 <Self as EnsureOrigin<O>>::try_origin(o)
1427 }
1428 #[cfg(feature = "runtime-benchmarks")]
1429 fn try_successful_origin(_: &T) -> Result<O, ()> {
1430 <Self as EnsureOrigin<O>>::try_successful_origin()
1431 }
1432}
1433
1434pub struct EnsureMembers<AccountId, I: 'static, const N: u32>(PhantomData<(AccountId, I)>);
1435impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32> EnsureOrigin<O>
1436 for EnsureMembers<AccountId, I, N>
1437where
1438 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1439{
1440 type Success = (MemberCount, MemberCount);
1441 fn try_origin(o: O) -> Result<Self::Success, O> {
1442 match o.caller().try_into() {
1443 Ok(RawOrigin::Members(n, m)) if *n >= N => return Ok((*n, *m)),
1444 _ => (),
1445 }
1446
1447 Err(o)
1448 }
1449
1450 #[cfg(feature = "runtime-benchmarks")]
1451 fn try_successful_origin() -> Result<O, ()> {
1452 Ok(O::from(RawOrigin::Members(N, N)))
1453 }
1454}
1455
1456impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, T>
1457 EnsureOriginWithArg<O, T> for EnsureMembers<AccountId, I, N>
1458where
1459 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1460{
1461 type Success = <Self as EnsureOrigin<O>>::Success;
1462 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1463 <Self as EnsureOrigin<O>>::try_origin(o)
1464 }
1465 #[cfg(feature = "runtime-benchmarks")]
1466 fn try_successful_origin(_: &T) -> Result<O, ()> {
1467 <Self as EnsureOrigin<O>>::try_successful_origin()
1468 }
1469}
1470
1471pub struct EnsureProportionMoreThan<AccountId, I: 'static, const N: u32, const D: u32>(
1472 PhantomData<(AccountId, I)>,
1473);
1474impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1475 EnsureOrigin<O> for EnsureProportionMoreThan<AccountId, I, N, D>
1476where
1477 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1478{
1479 type Success = ();
1480 fn try_origin(o: O) -> Result<Self::Success, O> {
1481 match o.caller().try_into() {
1482 Ok(RawOrigin::Members(n, m)) if n * D > N * m => return Ok(()),
1483 _ => (),
1484 }
1485
1486 Err(o)
1487 }
1488
1489 #[cfg(feature = "runtime-benchmarks")]
1490 fn try_successful_origin() -> Result<O, ()> {
1491 Ok(O::from(RawOrigin::Members(1u32, 0u32)))
1492 }
1493}
1494
1495impl<
1496 O: OriginTrait + From<RawOrigin<AccountId, I>>,
1497 AccountId,
1498 I,
1499 const N: u32,
1500 const D: u32,
1501 T,
1502 > EnsureOriginWithArg<O, T> for EnsureProportionMoreThan<AccountId, I, N, D>
1503where
1504 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1505{
1506 type Success = <Self as EnsureOrigin<O>>::Success;
1507 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1508 <Self as EnsureOrigin<O>>::try_origin(o)
1509 }
1510 #[cfg(feature = "runtime-benchmarks")]
1511 fn try_successful_origin(_: &T) -> Result<O, ()> {
1512 <Self as EnsureOrigin<O>>::try_successful_origin()
1513 }
1514}
1515
1516pub struct EnsureProportionAtLeast<AccountId, I: 'static, const N: u32, const D: u32>(
1517 PhantomData<(AccountId, I)>,
1518);
1519impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1520 EnsureOrigin<O> for EnsureProportionAtLeast<AccountId, I, N, D>
1521where
1522 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1523{
1524 type Success = ();
1525 fn try_origin(o: O) -> Result<Self::Success, O> {
1526 match o.caller().try_into() {
1527 Ok(RawOrigin::Members(n, m)) if n * D >= N * m => return Ok(()),
1528 _ => (),
1529 };
1530
1531 Err(o)
1532 }
1533
1534 #[cfg(feature = "runtime-benchmarks")]
1535 fn try_successful_origin() -> Result<O, ()> {
1536 Ok(O::from(RawOrigin::Members(0u32, 0u32)))
1537 }
1538}
1539
1540impl<
1541 O: OriginTrait + From<RawOrigin<AccountId, I>>,
1542 AccountId,
1543 I,
1544 const N: u32,
1545 const D: u32,
1546 T,
1547 > EnsureOriginWithArg<O, T> for EnsureProportionAtLeast<AccountId, I, N, D>
1548where
1549 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1550{
1551 type Success = <Self as EnsureOrigin<O>>::Success;
1552 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1553 <Self as EnsureOrigin<O>>::try_origin(o)
1554 }
1555 #[cfg(feature = "runtime-benchmarks")]
1556 fn try_successful_origin(_: &T) -> Result<O, ()> {
1557 <Self as EnsureOrigin<O>>::try_successful_origin()
1558 }
1559}