1#![recursion_limit = "256"]
28#![cfg_attr(not(feature = "std"), no_std)]
29
30extern crate alloc;
31
32use frame_support::{
33 dispatch::DispatchResult,
34 ensure,
35 traits::{
36 fungible, Currency, Get, LockIdentifier, LockableCurrency, PollStatus, Polling,
37 ReservableCurrency, WithdrawReasons,
38 },
39};
40use sp_runtime::{
41 traits::{AtLeast32BitUnsigned, Saturating, StaticLookup, Zero},
42 ArithmeticError, DispatchError, Perbill,
43};
44
45mod conviction;
46mod traits;
47mod types;
48mod vote;
49pub mod weights;
50
51pub use self::{
52 conviction::Conviction,
53 pallet::*,
54 traits::{Status, VotingHooks},
55 types::{Delegations, Tally, UnvoteScope},
56 vote::{AccountVote, Casting, Delegating, Vote, Voting},
57 weights::WeightInfo,
58};
59use sp_runtime::traits::BlockNumberProvider;
60
61#[cfg(test)]
62mod tests;
63
64#[cfg(feature = "runtime-benchmarks")]
65pub mod benchmarking;
66
67const CONVICTION_VOTING_ID: LockIdentifier = *b"pyconvot";
68
69pub type BlockNumberFor<T, I> =
70 <<T as Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
71
72type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
73pub type BalanceOf<T, I = ()> =
74 <<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
75pub type VotingOf<T, I = ()> = Voting<
76 BalanceOf<T, I>,
77 <T as frame_system::Config>::AccountId,
78 BlockNumberFor<T, I>,
79 PollIndexOf<T, I>,
80 <T as Config<I>>::MaxVotes,
81>;
82#[allow(dead_code)]
83type DelegatingOf<T, I = ()> =
84 Delegating<BalanceOf<T, I>, <T as frame_system::Config>::AccountId, BlockNumberFor<T, I>>;
85pub type TallyOf<T, I = ()> = Tally<BalanceOf<T, I>, <T as Config<I>>::MaxTurnout>;
86pub type VotesOf<T, I = ()> = BalanceOf<T, I>;
87pub type PollIndexOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Index;
88#[cfg(feature = "runtime-benchmarks")]
89pub type IndexOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Index;
90pub type ClassOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Class;
91
92#[frame_support::pallet]
93pub mod pallet {
94 use super::*;
95 use frame_support::{
96 pallet_prelude::{
97 DispatchResultWithPostInfo, IsType, StorageDoubleMap, StorageMap, ValueQuery,
98 },
99 traits::ClassCountOf,
100 Twox64Concat,
101 };
102 use frame_system::pallet_prelude::{ensure_signed, OriginFor};
103 use sp_runtime::BoundedVec;
104
105 #[pallet::pallet]
106 pub struct Pallet<T, I = ()>(_);
107
108 #[pallet::config]
109 pub trait Config<I: 'static = ()>: frame_system::Config + Sized {
110 #[allow(deprecated)]
112 type RuntimeEvent: From<Event<Self, I>>
113 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
114 type WeightInfo: WeightInfo;
116 type Currency: ReservableCurrency<Self::AccountId>
118 + LockableCurrency<Self::AccountId, Moment = BlockNumberFor<Self, I>>
119 + fungible::Inspect<Self::AccountId>;
120
121 type Polls: Polling<
123 TallyOf<Self, I>,
124 Votes = BalanceOf<Self, I>,
125 Moment = BlockNumberFor<Self, I>,
126 >;
127
128 type MaxTurnout: Get<BalanceOf<Self, I>>;
132
133 #[pallet::constant]
138 type MaxVotes: Get<u32>;
139
140 #[pallet::constant]
145 type VoteLockingPeriod: Get<BlockNumberFor<Self, I>>;
146 type BlockNumberProvider: BlockNumberProvider;
148 type VotingHooks: VotingHooks<Self::AccountId, PollIndexOf<Self, I>, BalanceOf<Self, I>>;
160 }
161
162 #[pallet::storage]
165 pub type VotingFor<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
166 _,
167 Twox64Concat,
168 T::AccountId,
169 Twox64Concat,
170 ClassOf<T, I>,
171 VotingOf<T, I>,
172 ValueQuery,
173 >;
174
175 #[pallet::storage]
179 pub type ClassLocksFor<T: Config<I>, I: 'static = ()> = StorageMap<
180 _,
181 Twox64Concat,
182 T::AccountId,
183 BoundedVec<(ClassOf<T, I>, BalanceOf<T, I>), ClassCountOf<T::Polls, TallyOf<T, I>>>,
184 ValueQuery,
185 >;
186
187 #[pallet::event]
188 #[pallet::generate_deposit(pub(super) fn deposit_event)]
189 pub enum Event<T: Config<I>, I: 'static = ()> {
190 Delegated(T::AccountId, T::AccountId, ClassOf<T, I>),
192 Undelegated(T::AccountId, ClassOf<T, I>),
194 Voted {
196 who: T::AccountId,
197 vote: AccountVote<BalanceOf<T, I>>,
198 poll_index: PollIndexOf<T, I>,
199 },
200 VoteRemoved {
202 who: T::AccountId,
203 vote: AccountVote<BalanceOf<T, I>>,
204 poll_index: PollIndexOf<T, I>,
205 },
206 VoteUnlocked { who: T::AccountId, class: ClassOf<T, I> },
208 }
209
210 #[pallet::error]
211 pub enum Error<T, I = ()> {
212 NotOngoing,
214 NotVoter,
216 NoPermission,
218 NoPermissionYet,
220 AlreadyDelegating,
222 AlreadyVoting,
225 InsufficientFunds,
227 NotDelegating,
229 Nonsense,
231 MaxVotesReached,
233 ClassNeeded,
235 BadClass,
237 }
238
239 #[pallet::call]
240 impl<T: Config<I>, I: 'static> Pallet<T, I> {
241 #[pallet::call_index(0)]
251 #[pallet::weight(T::WeightInfo::vote_new().max(T::WeightInfo::vote_existing()))]
252 pub fn vote(
253 origin: OriginFor<T>,
254 #[pallet::compact] poll_index: PollIndexOf<T, I>,
255 vote: AccountVote<BalanceOf<T, I>>,
256 ) -> DispatchResult {
257 let who = ensure_signed(origin)?;
258 Self::try_vote(&who, poll_index, vote)
259 }
260
261 #[pallet::call_index(1)]
287 #[pallet::weight(T::WeightInfo::delegate(T::MaxVotes::get()))]
288 pub fn delegate(
289 origin: OriginFor<T>,
290 class: ClassOf<T, I>,
291 to: AccountIdLookupOf<T>,
292 conviction: Conviction,
293 balance: BalanceOf<T, I>,
294 ) -> DispatchResultWithPostInfo {
295 let who = ensure_signed(origin)?;
296 let to = T::Lookup::lookup(to)?;
297 let votes = Self::try_delegate(who, class, to, conviction, balance)?;
298
299 Ok(Some(T::WeightInfo::delegate(votes)).into())
300 }
301
302 #[pallet::call_index(2)]
319 #[pallet::weight(T::WeightInfo::undelegate(T::MaxVotes::get().into()))]
320 pub fn undelegate(
321 origin: OriginFor<T>,
322 class: ClassOf<T, I>,
323 ) -> DispatchResultWithPostInfo {
324 let who = ensure_signed(origin)?;
325 let votes = Self::try_undelegate(who, class)?;
326 Ok(Some(T::WeightInfo::undelegate(votes)).into())
327 }
328
329 #[pallet::call_index(3)]
339 #[pallet::weight(T::WeightInfo::unlock())]
340 pub fn unlock(
341 origin: OriginFor<T>,
342 class: ClassOf<T, I>,
343 target: AccountIdLookupOf<T>,
344 ) -> DispatchResult {
345 ensure_signed(origin)?;
346 let target = T::Lookup::lookup(target)?;
347 Self::update_lock(&class, &target);
348 Self::deposit_event(Event::VoteUnlocked { who: target, class });
349 Ok(())
350 }
351
352 #[pallet::call_index(4)]
382 #[pallet::weight(T::WeightInfo::remove_vote())]
383 pub fn remove_vote(
384 origin: OriginFor<T>,
385 class: Option<ClassOf<T, I>>,
386 index: PollIndexOf<T, I>,
387 ) -> DispatchResult {
388 let who = ensure_signed(origin)?;
389 Self::try_remove_vote(&who, index, class, UnvoteScope::Any)
390 }
391
392 #[pallet::call_index(5)]
409 #[pallet::weight(T::WeightInfo::remove_other_vote())]
410 pub fn remove_other_vote(
411 origin: OriginFor<T>,
412 target: AccountIdLookupOf<T>,
413 class: ClassOf<T, I>,
414 index: PollIndexOf<T, I>,
415 ) -> DispatchResult {
416 let who = ensure_signed(origin)?;
417 let target = T::Lookup::lookup(target)?;
418 let scope = if target == who { UnvoteScope::Any } else { UnvoteScope::OnlyExpired };
419 Self::try_remove_vote(&target, index, Some(class), scope)?;
420 Ok(())
421 }
422 }
423}
424
425impl<T: Config<I>, I: 'static> Pallet<T, I> {
426 fn try_vote(
428 who: &T::AccountId,
429 poll_index: PollIndexOf<T, I>,
430 vote: AccountVote<BalanceOf<T, I>>,
431 ) -> DispatchResult {
432 ensure!(
433 vote.balance() <= T::Currency::total_balance(who),
434 Error::<T, I>::InsufficientFunds
435 );
436 T::VotingHooks::on_before_vote(who, poll_index, vote)?;
438
439 T::Polls::try_access_poll(poll_index, |poll_status| {
440 let (tally, class) = poll_status.ensure_ongoing().ok_or(Error::<T, I>::NotOngoing)?;
441 VotingFor::<T, I>::try_mutate(who, &class, |voting| {
442 if let Voting::Casting(Casting { ref mut votes, delegations, .. }) = voting {
443 match votes.binary_search_by_key(&poll_index, |i| i.0) {
444 Ok(i) => {
445 tally.remove(votes[i].1).ok_or(ArithmeticError::Underflow)?;
447 if let Some(approve) = votes[i].1.as_standard() {
448 tally.reduce(approve, *delegations);
449 }
450 votes[i].1 = vote;
451 },
452 Err(i) => {
453 votes
454 .try_insert(i, (poll_index, vote))
455 .map_err(|_| Error::<T, I>::MaxVotesReached)?;
456 },
457 }
458 tally.add(vote).ok_or(ArithmeticError::Overflow)?;
460 if let Some(approve) = vote.as_standard() {
461 tally.increase(approve, *delegations);
462 }
463 } else {
464 return Err(Error::<T, I>::AlreadyDelegating.into());
465 }
466 Self::extend_lock(who, &class, vote.balance());
469 Self::deposit_event(Event::Voted { who: who.clone(), vote, poll_index });
470 Ok(())
471 })
472 })
473 }
474
475 fn try_remove_vote(
482 who: &T::AccountId,
483 poll_index: PollIndexOf<T, I>,
484 class_hint: Option<ClassOf<T, I>>,
485 scope: UnvoteScope,
486 ) -> DispatchResult {
487 let class = class_hint
488 .or_else(|| Some(T::Polls::as_ongoing(poll_index)?.1))
489 .ok_or(Error::<T, I>::ClassNeeded)?;
490 VotingFor::<T, I>::try_mutate(who, class, |voting| {
491 if let Voting::Casting(Casting { ref mut votes, delegations, ref mut prior }) = voting {
492 let i = votes
493 .binary_search_by_key(&poll_index, |i| i.0)
494 .map_err(|_| Error::<T, I>::NotVoter)?;
495 let v = votes.remove(i);
496
497 T::Polls::try_access_poll(poll_index, |poll_status| match poll_status {
498 PollStatus::Ongoing(tally, _) => {
499 ensure!(matches!(scope, UnvoteScope::Any), Error::<T, I>::NoPermission);
500 tally.remove(v.1).ok_or(ArithmeticError::Underflow)?;
502 if let Some(approve) = v.1.as_standard() {
503 tally.reduce(approve, *delegations);
504 }
505 Self::deposit_event(Event::VoteRemoved {
506 who: who.clone(),
507 vote: v.1,
508 poll_index,
509 });
510 T::VotingHooks::on_remove_vote(who, poll_index, Status::Ongoing);
511 Ok(())
512 },
513 PollStatus::Completed(end, approved) => {
514 if let Some((lock_periods, balance)) =
515 v.1.locked_if(vote::LockedIf::Status(approved))
516 {
517 let unlock_at = end.saturating_add(
518 T::VoteLockingPeriod::get().saturating_mul(lock_periods.into()),
519 );
520 let now = T::BlockNumberProvider::current_block_number();
521 if now < unlock_at {
522 ensure!(
523 matches!(scope, UnvoteScope::Any),
524 Error::<T, I>::NoPermissionYet
525 );
526 prior.accumulate(unlock_at, balance)
527 }
528 } else if v.1.as_standard().is_some_and(|vote| vote != approved) {
529 if let Some(to_lock) =
532 T::VotingHooks::lock_balance_on_unsuccessful_vote(who, poll_index)
533 {
534 if let AccountVote::Standard { vote, .. } = v.1 {
535 let unlock_at = end.saturating_add(
536 T::VoteLockingPeriod::get()
537 .saturating_mul(vote.conviction.lock_periods().into()),
538 );
539 let now = T::BlockNumberProvider::current_block_number();
540 if now < unlock_at {
541 ensure!(
542 matches!(scope, UnvoteScope::Any),
543 Error::<T, I>::NoPermissionYet
544 );
545 prior.accumulate(unlock_at, to_lock)
546 }
547 }
548 }
549 }
550 T::VotingHooks::on_remove_vote(who, poll_index, Status::Completed);
552 Ok(())
553 },
554 PollStatus::None => {
555 T::VotingHooks::on_remove_vote(who, poll_index, Status::None);
557 Ok(())
558 },
559 })
560 } else {
561 Ok(())
562 }
563 })
564 }
565
566 fn increase_upstream_delegation(
568 who: &T::AccountId,
569 class: &ClassOf<T, I>,
570 amount: Delegations<BalanceOf<T, I>>,
571 ) -> u32 {
572 VotingFor::<T, I>::mutate(who, class, |voting| match voting {
573 Voting::Delegating(Delegating { delegations, .. }) => {
574 *delegations = delegations.saturating_add(amount);
576 1
577 },
578 Voting::Casting(Casting { votes, delegations, .. }) => {
579 *delegations = delegations.saturating_add(amount);
580 for &(poll_index, account_vote) in votes.iter() {
581 if let AccountVote::Standard { vote, .. } = account_vote {
582 T::Polls::access_poll(poll_index, |poll_status| {
583 if let PollStatus::Ongoing(tally, _) = poll_status {
584 tally.increase(vote.aye, amount);
585 }
586 });
587 }
588 }
589 votes.len() as u32
590 },
591 })
592 }
593
594 fn reduce_upstream_delegation(
596 who: &T::AccountId,
597 class: &ClassOf<T, I>,
598 amount: Delegations<BalanceOf<T, I>>,
599 ) -> u32 {
600 VotingFor::<T, I>::mutate(who, class, |voting| match voting {
601 Voting::Delegating(Delegating { delegations, .. }) => {
602 *delegations = delegations.saturating_sub(amount);
604 1
605 },
606 Voting::Casting(Casting { votes, delegations, .. }) => {
607 *delegations = delegations.saturating_sub(amount);
608 for &(poll_index, account_vote) in votes.iter() {
609 if let AccountVote::Standard { vote, .. } = account_vote {
610 T::Polls::access_poll(poll_index, |poll_status| {
611 if let PollStatus::Ongoing(tally, _) = poll_status {
612 tally.reduce(vote.aye, amount);
613 }
614 });
615 }
616 }
617 votes.len() as u32
618 },
619 })
620 }
621
622 fn try_delegate(
626 who: T::AccountId,
627 class: ClassOf<T, I>,
628 target: T::AccountId,
629 conviction: Conviction,
630 balance: BalanceOf<T, I>,
631 ) -> Result<u32, DispatchError> {
632 ensure!(who != target, Error::<T, I>::Nonsense);
633 T::Polls::classes().binary_search(&class).map_err(|_| Error::<T, I>::BadClass)?;
634 ensure!(balance <= T::Currency::total_balance(&who), Error::<T, I>::InsufficientFunds);
635 let votes =
636 VotingFor::<T, I>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
637 let old = core::mem::replace(
638 voting,
639 Voting::Delegating(Delegating {
640 balance,
641 target: target.clone(),
642 conviction,
643 delegations: Default::default(),
644 prior: Default::default(),
645 }),
646 );
647 match old {
648 Voting::Delegating(Delegating { .. }) =>
649 return Err(Error::<T, I>::AlreadyDelegating.into()),
650 Voting::Casting(Casting { votes, delegations, prior }) => {
651 ensure!(votes.is_empty(), Error::<T, I>::AlreadyVoting);
653 voting.set_common(delegations, prior);
654 },
655 }
656
657 let votes =
658 Self::increase_upstream_delegation(&target, &class, conviction.votes(balance));
659 Self::extend_lock(&who, &class, balance);
662 Ok(votes)
663 })?;
664 Self::deposit_event(Event::<T, I>::Delegated(who, target, class));
665 Ok(votes)
666 }
667
668 fn try_undelegate(who: T::AccountId, class: ClassOf<T, I>) -> Result<u32, DispatchError> {
672 let votes =
673 VotingFor::<T, I>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
674 match core::mem::replace(voting, Voting::default()) {
675 Voting::Delegating(Delegating {
676 balance,
677 target,
678 conviction,
679 delegations,
680 mut prior,
681 }) => {
682 let votes = Self::reduce_upstream_delegation(
684 &target,
685 &class,
686 conviction.votes(balance),
687 );
688 let now = T::BlockNumberProvider::current_block_number();
689 let lock_periods = conviction.lock_periods().into();
690 prior.accumulate(
691 now.saturating_add(
692 T::VoteLockingPeriod::get().saturating_mul(lock_periods),
693 ),
694 balance,
695 );
696 voting.set_common(delegations, prior);
697
698 Ok(votes)
699 },
700 Voting::Casting(_) => Err(Error::<T, I>::NotDelegating.into()),
701 }
702 })?;
703 Self::deposit_event(Event::<T, I>::Undelegated(who, class));
704 Ok(votes)
705 }
706
707 fn extend_lock(who: &T::AccountId, class: &ClassOf<T, I>, amount: BalanceOf<T, I>) {
708 ClassLocksFor::<T, I>::mutate(who, |locks| {
709 match locks.iter().position(|x| &x.0 == class) {
710 Some(i) => locks[i].1 = locks[i].1.max(amount),
711 None => {
712 let ok = locks.try_push((class.clone(), amount)).is_ok();
713 debug_assert!(
714 ok,
715 "Vec bounded by number of classes; \
716 all items in Vec associated with a unique class; \
717 qed"
718 );
719 },
720 }
721 });
722 T::Currency::extend_lock(
723 CONVICTION_VOTING_ID,
724 who,
725 amount,
726 WithdrawReasons::except(WithdrawReasons::RESERVE),
727 );
728 }
729
730 fn update_lock(class: &ClassOf<T, I>, who: &T::AccountId) {
733 let class_lock_needed = VotingFor::<T, I>::mutate(who, class, |voting| {
734 voting.rejig(T::BlockNumberProvider::current_block_number());
735 voting.locked_balance()
736 });
737 let lock_needed = ClassLocksFor::<T, I>::mutate(who, |locks| {
738 locks.retain(|x| &x.0 != class);
739 if !class_lock_needed.is_zero() {
740 let ok = locks.try_push((class.clone(), class_lock_needed)).is_ok();
741 debug_assert!(
742 ok,
743 "Vec bounded by number of classes; \
744 all items in Vec associated with a unique class; \
745 qed"
746 );
747 }
748 locks.iter().map(|x| x.1).max().unwrap_or(Zero::zero())
749 });
750 if lock_needed.is_zero() {
751 T::Currency::remove_lock(CONVICTION_VOTING_ID, who);
752 } else {
753 T::Currency::set_lock(
754 CONVICTION_VOTING_ID,
755 who,
756 lock_needed,
757 WithdrawReasons::except(WithdrawReasons::RESERVE),
758 );
759 }
760 }
761}