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 },
651 Voting::Casting(Casting { votes, delegations, prior }) => {
652 ensure!(votes.is_empty(), Error::<T, I>::AlreadyVoting);
654 voting.set_common(delegations, prior);
655 },
656 }
657
658 let votes =
659 Self::increase_upstream_delegation(&target, &class, conviction.votes(balance));
660 Self::extend_lock(&who, &class, balance);
663 Ok(votes)
664 })?;
665 Self::deposit_event(Event::<T, I>::Delegated(who, target, class));
666 Ok(votes)
667 }
668
669 fn try_undelegate(who: T::AccountId, class: ClassOf<T, I>) -> Result<u32, DispatchError> {
673 let votes =
674 VotingFor::<T, I>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
675 match core::mem::replace(voting, Voting::default()) {
676 Voting::Delegating(Delegating {
677 balance,
678 target,
679 conviction,
680 delegations,
681 mut prior,
682 }) => {
683 let votes = Self::reduce_upstream_delegation(
685 &target,
686 &class,
687 conviction.votes(balance),
688 );
689 let now = T::BlockNumberProvider::current_block_number();
690 let lock_periods = conviction.lock_periods().into();
691 prior.accumulate(
692 now.saturating_add(
693 T::VoteLockingPeriod::get().saturating_mul(lock_periods),
694 ),
695 balance,
696 );
697 voting.set_common(delegations, prior);
698
699 Ok(votes)
700 },
701 Voting::Casting(_) => Err(Error::<T, I>::NotDelegating.into()),
702 }
703 })?;
704 Self::deposit_event(Event::<T, I>::Undelegated(who, class));
705 Ok(votes)
706 }
707
708 fn extend_lock(who: &T::AccountId, class: &ClassOf<T, I>, amount: BalanceOf<T, I>) {
709 ClassLocksFor::<T, I>::mutate(who, |locks| {
710 match locks.iter().position(|x| &x.0 == class) {
711 Some(i) => locks[i].1 = locks[i].1.max(amount),
712 None => {
713 let ok = locks.try_push((class.clone(), amount)).is_ok();
714 debug_assert!(
715 ok,
716 "Vec bounded by number of classes; \
717 all items in Vec associated with a unique class; \
718 qed"
719 );
720 },
721 }
722 });
723 T::Currency::extend_lock(
724 CONVICTION_VOTING_ID,
725 who,
726 amount,
727 WithdrawReasons::except(WithdrawReasons::RESERVE),
728 );
729 }
730
731 fn update_lock(class: &ClassOf<T, I>, who: &T::AccountId) {
734 let class_lock_needed = VotingFor::<T, I>::mutate(who, class, |voting| {
735 voting.rejig(T::BlockNumberProvider::current_block_number());
736 voting.locked_balance()
737 });
738 let lock_needed = ClassLocksFor::<T, I>::mutate(who, |locks| {
739 locks.retain(|x| &x.0 != class);
740 if !class_lock_needed.is_zero() {
741 let ok = locks.try_push((class.clone(), class_lock_needed)).is_ok();
742 debug_assert!(
743 ok,
744 "Vec bounded by number of classes; \
745 all items in Vec associated with a unique class; \
746 qed"
747 );
748 }
749 locks.iter().map(|x| x.1).max().unwrap_or(Zero::zero())
750 });
751 if lock_needed.is_zero() {
752 T::Currency::remove_lock(CONVICTION_VOTING_ID, who);
753 } else {
754 T::Currency::set_lock(
755 CONVICTION_VOTING_ID,
756 who,
757 lock_needed,
758 WithdrawReasons::except(WithdrawReasons::RESERVE),
759 );
760 }
761 }
762}