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)]
190#[doc = docify::embed!("src/tests.rs", deposit_round_with_geometric_work)]
192pub mod deposit {
193 use core::marker::PhantomData;
194 use sp_core::Get;
195 use sp_runtime::{traits::Convert, FixedPointNumber, FixedU128, Saturating};
196
197 pub struct Constant<Deposit>(PhantomData<Deposit>);
200 impl<Deposit, Balance> Convert<u32, Balance> for Constant<Deposit>
201 where
202 Deposit: Get<Balance>,
203 Balance: frame_support::traits::tokens::Balance,
204 {
205 fn convert(_: u32) -> Balance {
206 Deposit::get()
207 }
208 }
209
210 pub struct Linear<Slope, Offset>(PhantomData<(Slope, Offset)>);
213 impl<Slope, Offset, Balance> Convert<u32, Balance> for Linear<Slope, Offset>
214 where
215 Slope: Get<u32>,
216 Offset: Get<Balance>,
217 Balance: frame_support::traits::tokens::Balance,
218 {
219 fn convert(proposal_count: u32) -> Balance {
220 let base: Balance = Slope::get().saturating_mul(proposal_count).into();
221 Offset::get().saturating_add(base)
222 }
223 }
224
225 pub struct Geometric<Ratio, Base>(PhantomData<(Ratio, Base)>);
228 impl<Ratio, Base, Balance> Convert<u32, Balance> for Geometric<Ratio, Base>
229 where
230 Ratio: Get<FixedU128>,
231 Base: Get<Balance>,
232 Balance: frame_support::traits::tokens::Balance,
233 {
234 fn convert(proposal_count: u32) -> Balance {
235 Ratio::get()
236 .saturating_pow(proposal_count as usize)
237 .saturating_mul_int(Base::get())
238 }
239 }
240
241 pub struct Round<Precision, Deposit>(PhantomData<(Precision, Deposit)>);
245 impl<Precision, Deposit, Balance> Convert<u32, Balance> for Round<Precision, Deposit>
246 where
247 Precision: Get<u32>,
248 Deposit: Convert<u32, Balance>,
249 Balance: frame_support::traits::tokens::Balance,
250 {
251 fn convert(proposal_count: u32) -> Balance {
252 let deposit = Deposit::convert(proposal_count);
253 if !deposit.is_zero() {
254 let factor: Balance =
255 Balance::from(10u32).saturating_pow(Precision::get() as usize);
256 if factor > deposit {
257 deposit
258 } else {
259 (deposit / factor) * factor
260 }
261 } else {
262 deposit
263 }
264 }
265 }
266
267 pub struct Stepped<Period, Step>(PhantomData<(Period, Step)>);
269 impl<Period, Step, Balance> Convert<u32, Balance> for Stepped<Period, Step>
270 where
271 Period: Get<u32>,
272 Step: Convert<u32, Balance>,
273 Balance: frame_support::traits::tokens::Balance,
274 {
275 fn convert(proposal_count: u32) -> Balance {
276 let step_num = proposal_count / Period::get();
277 Step::convert(step_num)
278 }
279 }
280
281 pub struct Delayed<Delay, Deposit>(PhantomData<(Delay, Deposit)>);
283 impl<Delay, Deposit, Balance> Convert<u32, Balance> for Delayed<Delay, Deposit>
284 where
285 Delay: Get<u32>,
286 Deposit: Convert<u32, Balance>,
287 Balance: frame_support::traits::tokens::Balance,
288 {
289 fn convert(proposal_count: u32) -> Balance {
290 let delay = Delay::get();
291 if delay > proposal_count {
292 return Balance::zero();
293 }
294 let pos = proposal_count.saturating_sub(delay);
295 Deposit::convert(pos)
296 }
297 }
298
299 pub struct WithCeil<Ceil, Deposit>(PhantomData<(Ceil, Deposit)>);
301 impl<Ceil, Deposit, Balance> Convert<u32, Balance> for WithCeil<Ceil, Deposit>
302 where
303 Ceil: Get<Balance>,
304 Deposit: Convert<u32, Balance>,
305 Balance: frame_support::traits::tokens::Balance,
306 {
307 fn convert(proposal_count: u32) -> Balance {
308 Deposit::convert(proposal_count).min(Ceil::get())
309 }
310 }
311}
312
313#[frame_support::pallet]
314pub mod pallet {
315 use super::*;
316 use frame_support::pallet_prelude::*;
317 use frame_system::pallet_prelude::*;
318
319 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
321
322 #[pallet::pallet]
323 #[pallet::storage_version(STORAGE_VERSION)]
324 #[pallet::without_storage_info]
325 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
326
327 #[pallet::config]
328 pub trait Config<I: 'static = ()>: frame_system::Config {
329 type RuntimeOrigin: From<RawOrigin<Self::AccountId, I>>;
331
332 type Proposal: Parameter
334 + Dispatchable<
335 RuntimeOrigin = <Self as Config<I>>::RuntimeOrigin,
336 PostInfo = PostDispatchInfo,
337 > + From<frame_system::Call<Self>>
338 + GetDispatchInfo;
339
340 #[allow(deprecated)]
342 type RuntimeEvent: From<Event<Self, I>>
343 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
344
345 type MotionDuration: Get<BlockNumberFor<Self>>;
347
348 type MaxProposals: Get<ProposalIndex>;
350
351 type MaxMembers: Get<MemberCount>;
357
358 type DefaultVote: DefaultVote;
360
361 type WeightInfo: WeightInfo;
363
364 type SetMembersOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
366
367 #[pallet::constant]
369 type MaxProposalWeight: Get<Weight>;
370
371 type DisapproveOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
374
375 type KillOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
380
381 type Consideration: MaybeConsideration<Self::AccountId, u32>;
391 }
392
393 #[pallet::genesis_config]
394 #[derive(frame_support::DefaultNoBound)]
395 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
396 #[serde(skip)]
397 pub phantom: PhantomData<I>,
398 pub members: Vec<T::AccountId>,
399 }
400
401 #[pallet::genesis_build]
402 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
403 fn build(&self) {
404 use alloc::collections::btree_set::BTreeSet;
405 let members_set: BTreeSet<_> = self.members.iter().collect();
406 assert_eq!(
407 members_set.len(),
408 self.members.len(),
409 "Members cannot contain duplicate accounts."
410 );
411 assert!(
412 self.members.len() <= T::MaxMembers::get() as usize,
413 "Members length cannot exceed MaxMembers.",
414 );
415
416 Pallet::<T, I>::initialize_members(&self.members)
417 }
418 }
419
420 #[pallet::origin]
422 pub type Origin<T, I = ()> = RawOrigin<<T as frame_system::Config>::AccountId, I>;
423
424 #[pallet::storage]
426 pub type Proposals<T: Config<I>, I: 'static = ()> =
427 StorageValue<_, BoundedVec<T::Hash, T::MaxProposals>, ValueQuery>;
428
429 #[pallet::storage]
431 pub type ProposalOf<T: Config<I>, I: 'static = ()> =
432 StorageMap<_, Identity, T::Hash, <T as Config<I>>::Proposal, OptionQuery>;
433
434 #[pallet::storage]
439 pub type CostOf<T: Config<I>, I: 'static = ()> =
440 StorageMap<_, Identity, T::Hash, (T::AccountId, T::Consideration), OptionQuery>;
441
442 #[pallet::storage]
444 pub type Voting<T: Config<I>, I: 'static = ()> =
445 StorageMap<_, Identity, T::Hash, Votes<T::AccountId, BlockNumberFor<T>>, OptionQuery>;
446
447 #[pallet::storage]
449 pub type ProposalCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
450
451 #[pallet::storage]
453 pub type Members<T: Config<I>, I: 'static = ()> =
454 StorageValue<_, Vec<T::AccountId>, ValueQuery>;
455
456 #[pallet::storage]
458 pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
459
460 #[pallet::event]
461 #[pallet::generate_deposit(pub(super) fn deposit_event)]
462 pub enum Event<T: Config<I>, I: 'static = ()> {
463 Proposed {
466 account: T::AccountId,
467 proposal_index: ProposalIndex,
468 proposal_hash: T::Hash,
469 threshold: MemberCount,
470 },
471 Voted {
474 account: T::AccountId,
475 proposal_hash: T::Hash,
476 voted: bool,
477 yes: MemberCount,
478 no: MemberCount,
479 },
480 Approved { proposal_hash: T::Hash },
482 Disapproved { proposal_hash: T::Hash },
484 Executed { proposal_hash: T::Hash, result: DispatchResult },
486 MemberExecuted { proposal_hash: T::Hash, result: DispatchResult },
488 Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount },
490 Killed { proposal_hash: T::Hash },
492 ProposalCostBurned { proposal_hash: T::Hash, who: T::AccountId },
494 ProposalCostReleased { proposal_hash: T::Hash, who: T::AccountId },
496 }
497
498 #[pallet::error]
499 pub enum Error<T, I = ()> {
500 NotMember,
502 DuplicateProposal,
504 ProposalMissing,
506 WrongIndex,
508 DuplicateVote,
510 AlreadyInitialized,
512 TooEarly,
514 TooManyProposals,
516 WrongProposalWeight,
518 WrongProposalLength,
520 PrimeAccountNotMember,
522 ProposalActive,
524 }
525
526 #[pallet::hooks]
527 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
528 #[cfg(feature = "try-runtime")]
529 fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
530 Self::do_try_state()
531 }
532 }
533
534 #[pallet::composite_enum]
536 pub enum HoldReason<I: 'static = ()> {
537 #[codec(index = 0)]
539 ProposalSubmission,
540 }
541
542 #[pallet::call]
544 impl<T: Config<I>, I: 'static> Pallet<T, I> {
545 #[pallet::call_index(0)]
570 #[pallet::weight((
571 T::WeightInfo::set_members(
572 *old_count, new_members.len() as u32, T::MaxProposals::get() ),
576 DispatchClass::Operational
577 ))]
578 pub fn set_members(
579 origin: OriginFor<T>,
580 new_members: Vec<T::AccountId>,
581 prime: Option<T::AccountId>,
582 old_count: MemberCount,
583 ) -> DispatchResultWithPostInfo {
584 T::SetMembersOrigin::ensure_origin(origin)?;
585 if new_members.len() > T::MaxMembers::get() as usize {
586 log::error!(
587 target: LOG_TARGET,
588 "New members count ({}) exceeds maximum amount of members expected ({}).",
589 new_members.len(),
590 T::MaxMembers::get(),
591 );
592 }
593
594 let old = Members::<T, I>::get();
595 if old.len() > old_count as usize {
596 log::warn!(
597 target: LOG_TARGET,
598 "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})",
599 old_count,
600 old.len(),
601 );
602 }
603 if let Some(p) = &prime {
604 ensure!(new_members.contains(p), Error::<T, I>::PrimeAccountNotMember);
605 }
606 let mut new_members = new_members;
607 new_members.sort();
608 <Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members, &old);
609 Prime::<T, I>::set(prime);
610
611 Ok(Some(T::WeightInfo::set_members(
612 old.len() as u32, new_members.len() as u32, T::MaxProposals::get(), ))
616 .into())
617 }
618
619 #[pallet::call_index(1)]
629 #[pallet::weight((
630 T::WeightInfo::execute(
631 *length_bound, T::MaxMembers::get(), ).saturating_add(proposal.get_dispatch_info().call_weight), DispatchClass::Operational
635 ))]
636 pub fn execute(
637 origin: OriginFor<T>,
638 proposal: Box<<T as Config<I>>::Proposal>,
639 #[pallet::compact] length_bound: u32,
640 ) -> DispatchResultWithPostInfo {
641 let who = ensure_signed(origin)?;
642 let members = Members::<T, I>::get();
643 ensure!(members.contains(&who), Error::<T, I>::NotMember);
644 let proposal_len = proposal.encoded_size();
645 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
646
647 let proposal_hash = T::Hashing::hash_of(&proposal);
648 let result = proposal.dispatch(RawOrigin::Member(who).into());
649 Self::deposit_event(Event::MemberExecuted {
650 proposal_hash,
651 result: result.map(|_| ()).map_err(|e| e.error),
652 });
653
654 Ok(get_result_weight(result)
655 .map(|w| {
656 T::WeightInfo::execute(
657 proposal_len as u32, members.len() as u32, )
660 .saturating_add(w) })
662 .into())
663 }
664
665 #[pallet::call_index(2)]
680 #[pallet::weight((
681 if *threshold < 2 {
682 T::WeightInfo::propose_execute(
683 *length_bound, T::MaxMembers::get(), ).saturating_add(proposal.get_dispatch_info().call_weight) } else {
687 T::WeightInfo::propose_proposed(
688 *length_bound, T::MaxMembers::get(), T::MaxProposals::get(), )
692 },
693 DispatchClass::Operational
694 ))]
695 pub fn propose(
696 origin: OriginFor<T>,
697 #[pallet::compact] threshold: MemberCount,
698 proposal: Box<<T as Config<I>>::Proposal>,
699 #[pallet::compact] length_bound: u32,
700 ) -> DispatchResultWithPostInfo {
701 let who = ensure_signed(origin)?;
702 let members = Members::<T, I>::get();
703 ensure!(members.contains(&who), Error::<T, I>::NotMember);
704
705 if threshold < 2 {
706 let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?;
707
708 Ok(get_result_weight(result)
709 .map(|w| {
710 T::WeightInfo::propose_execute(
711 proposal_len as u32, members.len() as u32, )
714 .saturating_add(w) })
716 .into())
717 } else {
718 let (proposal_len, active_proposals) =
719 Self::do_propose_proposed(who, threshold, proposal, length_bound)?;
720
721 Ok(Some(T::WeightInfo::propose_proposed(
722 proposal_len as u32, members.len() as u32, active_proposals, ))
726 .into())
727 }
728 }
729
730 #[pallet::call_index(3)]
740 #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))]
741 pub fn vote(
742 origin: OriginFor<T>,
743 proposal: T::Hash,
744 #[pallet::compact] index: ProposalIndex,
745 approve: bool,
746 ) -> DispatchResultWithPostInfo {
747 let who = ensure_signed(origin)?;
748 let members = Members::<T, I>::get();
749 ensure!(members.contains(&who), Error::<T, I>::NotMember);
750
751 let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?;
753
754 if is_account_voting_first_time {
755 Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into())
756 } else {
757 Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into())
758 }
759 }
760
761 #[pallet::call_index(5)]
774 #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))]
775 pub fn disapprove_proposal(
776 origin: OriginFor<T>,
777 proposal_hash: T::Hash,
778 ) -> DispatchResultWithPostInfo {
779 T::DisapproveOrigin::ensure_origin(origin)?;
780 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
781 Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into())
782 }
783
784 #[pallet::call_index(6)]
809 #[pallet::weight((
810 {
811 let b = *length_bound;
812 let m = T::MaxMembers::get();
813 let p1 = *proposal_weight_bound;
814 let p2 = T::MaxProposals::get();
815 T::WeightInfo::close_early_approved(b, m, p2)
816 .max(T::WeightInfo::close_early_disapproved(m, p2))
817 .max(T::WeightInfo::close_approved(b, m, p2))
818 .max(T::WeightInfo::close_disapproved(m, p2))
819 .saturating_add(p1)
820 },
821 DispatchClass::Operational
822 ))]
823 pub fn close(
824 origin: OriginFor<T>,
825 proposal_hash: T::Hash,
826 #[pallet::compact] index: ProposalIndex,
827 proposal_weight_bound: Weight,
828 #[pallet::compact] length_bound: u32,
829 ) -> DispatchResultWithPostInfo {
830 ensure_signed(origin)?;
831
832 Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
833 }
834
835 #[pallet::call_index(7)]
843 #[pallet::weight(T::WeightInfo::kill(1, T::MaxProposals::get()))]
844 pub fn kill(origin: OriginFor<T>, proposal_hash: T::Hash) -> DispatchResultWithPostInfo {
845 T::KillOrigin::ensure_origin(origin)?;
846 ensure!(
847 ProposalOf::<T, I>::get(&proposal_hash).is_some(),
848 Error::<T, I>::ProposalMissing
849 );
850 let burned = if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
851 cost.burn(&who);
852 Self::deposit_event(Event::ProposalCostBurned { proposal_hash, who });
853 true
854 } else {
855 false
856 };
857 let proposal_count = Self::remove_proposal(proposal_hash);
858
859 Self::deposit_event(Event::Killed { proposal_hash });
860
861 Ok(Some(T::WeightInfo::kill(burned as u32, proposal_count)).into())
862 }
863
864 #[pallet::call_index(8)]
874 #[pallet::weight(T::WeightInfo::release_proposal_cost())]
875 pub fn release_proposal_cost(
876 origin: OriginFor<T>,
877 proposal_hash: T::Hash,
878 ) -> DispatchResult {
879 ensure_signed_or_root(origin)?;
880 ensure!(
881 ProposalOf::<T, I>::get(&proposal_hash).is_none(),
882 Error::<T, I>::ProposalActive
883 );
884 if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
885 cost.drop(&who)?;
886 Self::deposit_event(Event::ProposalCostReleased { proposal_hash, who });
887 }
888
889 Ok(())
890 }
891 }
892}
893
894fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
898 match result {
899 Ok(post_info) => post_info.actual_weight,
900 Err(err) => err.post_info.actual_weight,
901 }
902}
903
904impl<T: Config<I>, I: 'static> Pallet<T, I> {
905 pub fn is_member(who: &T::AccountId) -> bool {
907 Members::<T, I>::get().contains(who)
910 }
911
912 pub fn do_propose_execute(
914 proposal: Box<<T as Config<I>>::Proposal>,
915 length_bound: MemberCount,
916 ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> {
917 let proposal_len = proposal.encoded_size();
918 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
919 let proposal_weight = proposal.get_dispatch_info().call_weight;
920 ensure!(
921 proposal_weight.all_lte(T::MaxProposalWeight::get()),
922 Error::<T, I>::WrongProposalWeight
923 );
924
925 let proposal_hash = T::Hashing::hash_of(&proposal);
926 ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
927
928 let seats = Members::<T, I>::get().len() as MemberCount;
929 let result = proposal.dispatch(RawOrigin::Members(1, seats).into());
930 Self::deposit_event(Event::Executed {
931 proposal_hash,
932 result: result.map(|_| ()).map_err(|e| e.error),
933 });
934 Ok((proposal_len as u32, result))
935 }
936
937 pub fn do_propose_proposed(
939 who: T::AccountId,
940 threshold: MemberCount,
941 proposal: Box<<T as Config<I>>::Proposal>,
942 length_bound: MemberCount,
943 ) -> Result<(u32, u32), DispatchError> {
944 let proposal_len = proposal.encoded_size();
945 ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
946 let proposal_weight = proposal.get_dispatch_info().call_weight;
947 ensure!(
948 proposal_weight.all_lte(T::MaxProposalWeight::get()),
949 Error::<T, I>::WrongProposalWeight
950 );
951
952 let proposal_hash = T::Hashing::hash_of(&proposal);
953 ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
954
955 let active_proposals =
956 <Proposals<T, I>>::try_mutate(|proposals| -> Result<usize, DispatchError> {
957 proposals.try_push(proposal_hash).map_err(|_| Error::<T, I>::TooManyProposals)?;
958 Ok(proposals.len())
959 })?;
960
961 let cost = T::Consideration::new(&who, active_proposals as u32 - 1)?;
962 if !cost.is_none() {
963 <CostOf<T, I>>::insert(proposal_hash, (who.clone(), cost));
964 }
965
966 let index = ProposalCount::<T, I>::get();
967
968 <ProposalCount<T, I>>::mutate(|i| *i += 1);
969 <ProposalOf<T, I>>::insert(proposal_hash, proposal);
970 let votes = {
971 let end = frame_system::Pallet::<T>::block_number() + T::MotionDuration::get();
972 Votes { index, threshold, ayes: vec![], nays: vec![], end }
973 };
974 <Voting<T, I>>::insert(proposal_hash, votes);
975
976 Self::deposit_event(Event::Proposed {
977 account: who,
978 proposal_index: index,
979 proposal_hash,
980 threshold,
981 });
982 Ok((proposal_len as u32, active_proposals as u32))
983 }
984
985 pub fn do_vote(
988 who: T::AccountId,
989 proposal: T::Hash,
990 index: ProposalIndex,
991 approve: bool,
992 ) -> Result<bool, DispatchError> {
993 let mut voting = Voting::<T, I>::get(&proposal).ok_or(Error::<T, I>::ProposalMissing)?;
994 ensure!(voting.index == index, Error::<T, I>::WrongIndex);
995
996 let position_yes = voting.ayes.iter().position(|a| a == &who);
997 let position_no = voting.nays.iter().position(|a| a == &who);
998
999 let is_account_voting_first_time = position_yes.is_none() && position_no.is_none();
1001
1002 if approve {
1003 if position_yes.is_none() {
1004 voting.ayes.push(who.clone());
1005 } else {
1006 return Err(Error::<T, I>::DuplicateVote.into());
1007 }
1008 if let Some(pos) = position_no {
1009 voting.nays.swap_remove(pos);
1010 }
1011 } else {
1012 if position_no.is_none() {
1013 voting.nays.push(who.clone());
1014 } else {
1015 return Err(Error::<T, I>::DuplicateVote.into());
1016 }
1017 if let Some(pos) = position_yes {
1018 voting.ayes.swap_remove(pos);
1019 }
1020 }
1021
1022 let yes_votes = voting.ayes.len() as MemberCount;
1023 let no_votes = voting.nays.len() as MemberCount;
1024 Self::deposit_event(Event::Voted {
1025 account: who,
1026 proposal_hash: proposal,
1027 voted: approve,
1028 yes: yes_votes,
1029 no: no_votes,
1030 });
1031
1032 Voting::<T, I>::insert(&proposal, voting);
1033
1034 Ok(is_account_voting_first_time)
1035 }
1036
1037 pub fn do_close(
1039 proposal_hash: T::Hash,
1040 index: ProposalIndex,
1041 proposal_weight_bound: Weight,
1042 length_bound: u32,
1043 ) -> DispatchResultWithPostInfo {
1044 let voting = Voting::<T, I>::get(&proposal_hash).ok_or(Error::<T, I>::ProposalMissing)?;
1045 ensure!(voting.index == index, Error::<T, I>::WrongIndex);
1046
1047 let mut no_votes = voting.nays.len() as MemberCount;
1048 let mut yes_votes = voting.ayes.len() as MemberCount;
1049 let seats = Members::<T, I>::get().len() as MemberCount;
1050 let approved = yes_votes >= voting.threshold;
1051 let disapproved = seats.saturating_sub(no_votes) < voting.threshold;
1052 if approved {
1054 let (proposal, len) = Self::validate_and_get_proposal(
1055 &proposal_hash,
1056 length_bound,
1057 proposal_weight_bound,
1058 )?;
1059 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1060 let (proposal_weight, proposal_count) =
1061 Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1062 return Ok((
1063 Some(
1064 T::WeightInfo::close_early_approved(len as u32, seats, proposal_count)
1065 .saturating_add(proposal_weight),
1066 ),
1067 Pays::Yes,
1068 )
1069 .into());
1070 } else if disapproved {
1071 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1072 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1073 return Ok((
1074 Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)),
1075 Pays::No,
1076 )
1077 .into());
1078 }
1079
1080 ensure!(frame_system::Pallet::<T>::block_number() >= voting.end, Error::<T, I>::TooEarly);
1082
1083 let prime_vote = Prime::<T, I>::get().map(|who| voting.ayes.iter().any(|a| a == &who));
1084
1085 let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats);
1087
1088 let abstentions = seats - (yes_votes + no_votes);
1089 match default {
1090 true => yes_votes += abstentions,
1091 false => no_votes += abstentions,
1092 }
1093 let approved = yes_votes >= voting.threshold;
1094
1095 if approved {
1096 let (proposal, len) = Self::validate_and_get_proposal(
1097 &proposal_hash,
1098 length_bound,
1099 proposal_weight_bound,
1100 )?;
1101 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1102 let (proposal_weight, proposal_count) =
1103 Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1104 Ok((
1105 Some(
1106 T::WeightInfo::close_approved(len as u32, seats, proposal_count)
1107 .saturating_add(proposal_weight),
1108 ),
1109 Pays::Yes,
1110 )
1111 .into())
1112 } else {
1113 Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1114 let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1115 Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into())
1116 }
1117 }
1118
1119 fn validate_and_get_proposal(
1124 hash: &T::Hash,
1125 length_bound: u32,
1126 weight_bound: Weight,
1127 ) -> Result<(<T as Config<I>>::Proposal, usize), DispatchError> {
1128 let key = ProposalOf::<T, I>::hashed_key_for(hash);
1129 let proposal_len =
1131 storage::read(&key, &mut [0; 0], 0).ok_or(Error::<T, I>::ProposalMissing)?;
1132 ensure!(proposal_len <= length_bound, Error::<T, I>::WrongProposalLength);
1133 let proposal = ProposalOf::<T, I>::get(hash).ok_or(Error::<T, I>::ProposalMissing)?;
1134 let proposal_weight = proposal.get_dispatch_info().call_weight;
1135 ensure!(proposal_weight.all_lte(weight_bound), Error::<T, I>::WrongProposalWeight);
1136 Ok((proposal, proposal_len as usize))
1137 }
1138
1139 fn do_approve_proposal(
1154 seats: MemberCount,
1155 yes_votes: MemberCount,
1156 proposal_hash: T::Hash,
1157 proposal: <T as Config<I>>::Proposal,
1158 ) -> (Weight, u32) {
1159 Self::deposit_event(Event::Approved { proposal_hash });
1160
1161 let dispatch_weight = proposal.get_dispatch_info().call_weight;
1162 let origin = RawOrigin::Members(yes_votes, seats).into();
1163 let result = proposal.dispatch(origin);
1164 Self::deposit_event(Event::Executed {
1165 proposal_hash,
1166 result: result.map(|_| ()).map_err(|e| e.error),
1167 });
1168 let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); let proposal_count = Self::remove_proposal(proposal_hash);
1172 (proposal_weight, proposal_count)
1173 }
1174
1175 pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 {
1177 Self::deposit_event(Event::Disapproved { proposal_hash });
1179 Self::remove_proposal(proposal_hash)
1180 }
1181
1182 fn remove_proposal(proposal_hash: T::Hash) -> u32 {
1184 ProposalOf::<T, I>::remove(&proposal_hash);
1186 Voting::<T, I>::remove(&proposal_hash);
1187 let num_proposals = Proposals::<T, I>::mutate(|proposals| {
1188 proposals.retain(|h| h != &proposal_hash);
1189 proposals.len() + 1 });
1191 num_proposals as u32
1192 }
1193
1194 #[cfg(any(feature = "try-runtime", test))]
1223 fn do_try_state() -> Result<(), TryRuntimeError> {
1224 Proposals::<T, I>::get().into_iter().try_for_each(
1225 |proposal| -> Result<(), TryRuntimeError> {
1226 ensure!(
1227 ProposalOf::<T, I>::get(proposal).is_some(),
1228 "Proposal hash from `Proposals` is not found inside the `ProposalOf` mapping."
1229 );
1230 Ok(())
1231 },
1232 )?;
1233
1234 ensure!(
1235 Proposals::<T, I>::get().into_iter().count() <= ProposalCount::<T, I>::get() as usize,
1236 "The actual number of proposals is greater than `ProposalCount`"
1237 );
1238 ensure!(
1239 Proposals::<T, I>::get().into_iter().count() == <ProposalOf<T, I>>::iter_keys().count(),
1240 "Proposal count inside `Proposals` is not equal to the proposal count in `ProposalOf`"
1241 );
1242
1243 Proposals::<T, I>::get().into_iter().try_for_each(
1244 |proposal| -> Result<(), TryRuntimeError> {
1245 if let Some(votes) = Voting::<T, I>::get(proposal) {
1246 let ayes = votes.ayes.len();
1247 let nays = votes.nays.len();
1248
1249 ensure!(
1250 ayes.saturating_add(nays) <= T::MaxMembers::get() as usize,
1251 "The sum of ayes and nays is greater than `MaxMembers`"
1252 );
1253 }
1254 Ok(())
1255 },
1256 )?;
1257
1258 let mut proposal_indices = vec![];
1259 Proposals::<T, I>::get().into_iter().try_for_each(
1260 |proposal| -> Result<(), TryRuntimeError> {
1261 if let Some(votes) = Voting::<T, I>::get(proposal) {
1262 let proposal_index = votes.index;
1263 ensure!(
1264 !proposal_indices.contains(&proposal_index),
1265 "The proposal index is not unique."
1266 );
1267 proposal_indices.push(proposal_index);
1268 }
1269 Ok(())
1270 },
1271 )?;
1272
1273 <Voting<T, I>>::iter_keys().try_for_each(
1274 |proposal_hash| -> Result<(), TryRuntimeError> {
1275 ensure!(
1276 Proposals::<T, I>::get().contains(&proposal_hash),
1277 "`Proposals` doesn't contain the proposal hash from the `Voting` storage map."
1278 );
1279 Ok(())
1280 },
1281 )?;
1282
1283 ensure!(
1284 Members::<T, I>::get().len() <= T::MaxMembers::get() as usize,
1285 "The member count is greater than `MaxMembers`."
1286 );
1287
1288 ensure!(
1289 Members::<T, I>::get().windows(2).all(|members| members[0] <= members[1]),
1290 "The members are not sorted by value."
1291 );
1292
1293 if let Some(prime) = Prime::<T, I>::get() {
1294 ensure!(Members::<T, I>::get().contains(&prime), "Prime account is not a member.");
1295 }
1296
1297 Ok(())
1298 }
1299}
1300
1301impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> {
1302 fn change_members_sorted(
1313 _incoming: &[T::AccountId],
1314 outgoing: &[T::AccountId],
1315 new: &[T::AccountId],
1316 ) {
1317 if new.len() > T::MaxMembers::get() as usize {
1318 log::error!(
1319 target: LOG_TARGET,
1320 "New members count ({}) exceeds maximum amount of members expected ({}).",
1321 new.len(),
1322 T::MaxMembers::get(),
1323 );
1324 }
1325 let mut outgoing = outgoing.to_vec();
1327 outgoing.sort();
1328 for h in Proposals::<T, I>::get().into_iter() {
1329 <Voting<T, I>>::mutate(h, |v| {
1330 if let Some(mut votes) = v.take() {
1331 votes.ayes = votes
1332 .ayes
1333 .into_iter()
1334 .filter(|i| outgoing.binary_search(i).is_err())
1335 .collect();
1336 votes.nays = votes
1337 .nays
1338 .into_iter()
1339 .filter(|i| outgoing.binary_search(i).is_err())
1340 .collect();
1341 *v = Some(votes);
1342 }
1343 });
1344 }
1345 Members::<T, I>::put(new);
1346 Prime::<T, I>::kill();
1347 }
1348
1349 fn set_prime(prime: Option<T::AccountId>) {
1350 Prime::<T, I>::set(prime);
1351 }
1352
1353 fn get_prime() -> Option<T::AccountId> {
1354 Prime::<T, I>::get()
1355 }
1356}
1357
1358impl<T: Config<I>, I: 'static> InitializeMembers<T::AccountId> for Pallet<T, I> {
1359 fn initialize_members(members: &[T::AccountId]) {
1360 if !members.is_empty() {
1361 assert!(<Members<T, I>>::get().is_empty(), "Members are already initialized!");
1362 let mut members = members.to_vec();
1363 members.sort();
1364 <Members<T, I>>::put(members);
1365 }
1366 }
1367}
1368
1369pub fn ensure_members<OuterOrigin, AccountId, I>(
1372 o: OuterOrigin,
1373 n: MemberCount,
1374) -> result::Result<MemberCount, &'static str>
1375where
1376 OuterOrigin: Into<result::Result<RawOrigin<AccountId, I>, OuterOrigin>>,
1377{
1378 match o.into() {
1379 Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n),
1380 _ => Err("bad origin: expected to be a threshold number of members"),
1381 }
1382}
1383
1384pub struct EnsureMember<AccountId, I: 'static>(PhantomData<(AccountId, I)>);
1385impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone> EnsureOrigin<O>
1386 for EnsureMember<AccountId, I>
1387where
1388 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1389{
1390 type Success = AccountId;
1391 fn try_origin(o: O) -> Result<Self::Success, O> {
1392 match o.caller().try_into() {
1393 Ok(RawOrigin::Member(id)) => return Ok(id.clone()),
1394 _ => (),
1395 }
1396
1397 Err(o)
1398 }
1399
1400 #[cfg(feature = "runtime-benchmarks")]
1401 fn try_successful_origin() -> Result<O, ()> {
1402 let zero_account_id =
1403 AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
1404 .expect("infinite length input; no invalid inputs for type; qed");
1405 Ok(O::from(RawOrigin::Member(zero_account_id)))
1406 }
1407}
1408
1409impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone, T>
1410 EnsureOriginWithArg<O, T> for EnsureMember<AccountId, I>
1411where
1412 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1413{
1414 type Success = <Self as EnsureOrigin<O>>::Success;
1415 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1416 <Self as EnsureOrigin<O>>::try_origin(o)
1417 }
1418 #[cfg(feature = "runtime-benchmarks")]
1419 fn try_successful_origin(_: &T) -> Result<O, ()> {
1420 <Self as EnsureOrigin<O>>::try_successful_origin()
1421 }
1422}
1423
1424pub struct EnsureMembers<AccountId, I: 'static, const N: u32>(PhantomData<(AccountId, I)>);
1425impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32> EnsureOrigin<O>
1426 for EnsureMembers<AccountId, I, N>
1427where
1428 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1429{
1430 type Success = (MemberCount, MemberCount);
1431 fn try_origin(o: O) -> Result<Self::Success, O> {
1432 match o.caller().try_into() {
1433 Ok(RawOrigin::Members(n, m)) if *n >= N => return Ok((*n, *m)),
1434 _ => (),
1435 }
1436
1437 Err(o)
1438 }
1439
1440 #[cfg(feature = "runtime-benchmarks")]
1441 fn try_successful_origin() -> Result<O, ()> {
1442 Ok(O::from(RawOrigin::Members(N, N)))
1443 }
1444}
1445
1446impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, T>
1447 EnsureOriginWithArg<O, T> for EnsureMembers<AccountId, I, N>
1448where
1449 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1450{
1451 type Success = <Self as EnsureOrigin<O>>::Success;
1452 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1453 <Self as EnsureOrigin<O>>::try_origin(o)
1454 }
1455 #[cfg(feature = "runtime-benchmarks")]
1456 fn try_successful_origin(_: &T) -> Result<O, ()> {
1457 <Self as EnsureOrigin<O>>::try_successful_origin()
1458 }
1459}
1460
1461pub struct EnsureProportionMoreThan<AccountId, I: 'static, const N: u32, const D: u32>(
1462 PhantomData<(AccountId, I)>,
1463);
1464impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1465 EnsureOrigin<O> for EnsureProportionMoreThan<AccountId, I, N, D>
1466where
1467 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1468{
1469 type Success = ();
1470 fn try_origin(o: O) -> Result<Self::Success, O> {
1471 match o.caller().try_into() {
1472 Ok(RawOrigin::Members(n, m)) if n * D > N * m => return Ok(()),
1473 _ => (),
1474 }
1475
1476 Err(o)
1477 }
1478
1479 #[cfg(feature = "runtime-benchmarks")]
1480 fn try_successful_origin() -> Result<O, ()> {
1481 Ok(O::from(RawOrigin::Members(1u32, 0u32)))
1482 }
1483}
1484
1485impl<
1486 O: OriginTrait + From<RawOrigin<AccountId, I>>,
1487 AccountId,
1488 I,
1489 const N: u32,
1490 const D: u32,
1491 T,
1492 > EnsureOriginWithArg<O, T> for EnsureProportionMoreThan<AccountId, I, N, D>
1493where
1494 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1495{
1496 type Success = <Self as EnsureOrigin<O>>::Success;
1497 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1498 <Self as EnsureOrigin<O>>::try_origin(o)
1499 }
1500 #[cfg(feature = "runtime-benchmarks")]
1501 fn try_successful_origin(_: &T) -> Result<O, ()> {
1502 <Self as EnsureOrigin<O>>::try_successful_origin()
1503 }
1504}
1505
1506pub struct EnsureProportionAtLeast<AccountId, I: 'static, const N: u32, const D: u32>(
1507 PhantomData<(AccountId, I)>,
1508);
1509impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1510 EnsureOrigin<O> for EnsureProportionAtLeast<AccountId, I, N, D>
1511where
1512 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1513{
1514 type Success = ();
1515 fn try_origin(o: O) -> Result<Self::Success, O> {
1516 match o.caller().try_into() {
1517 Ok(RawOrigin::Members(n, m)) if n * D >= N * m => return Ok(()),
1518 _ => (),
1519 };
1520
1521 Err(o)
1522 }
1523
1524 #[cfg(feature = "runtime-benchmarks")]
1525 fn try_successful_origin() -> Result<O, ()> {
1526 Ok(O::from(RawOrigin::Members(0u32, 0u32)))
1527 }
1528}
1529
1530impl<
1531 O: OriginTrait + From<RawOrigin<AccountId, I>>,
1532 AccountId,
1533 I,
1534 const N: u32,
1535 const D: u32,
1536 T,
1537 > EnsureOriginWithArg<O, T> for EnsureProportionAtLeast<AccountId, I, N, D>
1538where
1539 for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1540{
1541 type Success = <Self as EnsureOrigin<O>>::Success;
1542 fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1543 <Self as EnsureOrigin<O>>::try_origin(o)
1544 }
1545 #[cfg(feature = "runtime-benchmarks")]
1546 fn try_successful_origin(_: &T) -> Result<O, ()> {
1547 <Self as EnsureOrigin<O>>::try_successful_origin()
1548 }
1549}