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),
192 Undelegated(T::AccountId),
194 Voted { who: T::AccountId, vote: AccountVote<BalanceOf<T, I>> },
196 VoteRemoved { who: T::AccountId, vote: AccountVote<BalanceOf<T, I>> },
198 VoteUnlocked { who: T::AccountId, class: ClassOf<T, I> },
200 }
201
202 #[pallet::error]
203 pub enum Error<T, I = ()> {
204 NotOngoing,
206 NotVoter,
208 NoPermission,
210 NoPermissionYet,
212 AlreadyDelegating,
214 AlreadyVoting,
217 InsufficientFunds,
219 NotDelegating,
221 Nonsense,
223 MaxVotesReached,
225 ClassNeeded,
227 BadClass,
229 }
230
231 #[pallet::call]
232 impl<T: Config<I>, I: 'static> Pallet<T, I> {
233 #[pallet::call_index(0)]
243 #[pallet::weight(T::WeightInfo::vote_new().max(T::WeightInfo::vote_existing()))]
244 pub fn vote(
245 origin: OriginFor<T>,
246 #[pallet::compact] poll_index: PollIndexOf<T, I>,
247 vote: AccountVote<BalanceOf<T, I>>,
248 ) -> DispatchResult {
249 let who = ensure_signed(origin)?;
250 Self::try_vote(&who, poll_index, vote)
251 }
252
253 #[pallet::call_index(1)]
279 #[pallet::weight(T::WeightInfo::delegate(T::MaxVotes::get()))]
280 pub fn delegate(
281 origin: OriginFor<T>,
282 class: ClassOf<T, I>,
283 to: AccountIdLookupOf<T>,
284 conviction: Conviction,
285 balance: BalanceOf<T, I>,
286 ) -> DispatchResultWithPostInfo {
287 let who = ensure_signed(origin)?;
288 let to = T::Lookup::lookup(to)?;
289 let votes = Self::try_delegate(who, class, to, conviction, balance)?;
290
291 Ok(Some(T::WeightInfo::delegate(votes)).into())
292 }
293
294 #[pallet::call_index(2)]
311 #[pallet::weight(T::WeightInfo::undelegate(T::MaxVotes::get().into()))]
312 pub fn undelegate(
313 origin: OriginFor<T>,
314 class: ClassOf<T, I>,
315 ) -> DispatchResultWithPostInfo {
316 let who = ensure_signed(origin)?;
317 let votes = Self::try_undelegate(who, class)?;
318 Ok(Some(T::WeightInfo::undelegate(votes)).into())
319 }
320
321 #[pallet::call_index(3)]
331 #[pallet::weight(T::WeightInfo::unlock())]
332 pub fn unlock(
333 origin: OriginFor<T>,
334 class: ClassOf<T, I>,
335 target: AccountIdLookupOf<T>,
336 ) -> DispatchResult {
337 ensure_signed(origin)?;
338 let target = T::Lookup::lookup(target)?;
339 Self::update_lock(&class, &target);
340 Self::deposit_event(Event::VoteUnlocked { who: target, class });
341 Ok(())
342 }
343
344 #[pallet::call_index(4)]
374 #[pallet::weight(T::WeightInfo::remove_vote())]
375 pub fn remove_vote(
376 origin: OriginFor<T>,
377 class: Option<ClassOf<T, I>>,
378 index: PollIndexOf<T, I>,
379 ) -> DispatchResult {
380 let who = ensure_signed(origin)?;
381 Self::try_remove_vote(&who, index, class, UnvoteScope::Any)
382 }
383
384 #[pallet::call_index(5)]
401 #[pallet::weight(T::WeightInfo::remove_other_vote())]
402 pub fn remove_other_vote(
403 origin: OriginFor<T>,
404 target: AccountIdLookupOf<T>,
405 class: ClassOf<T, I>,
406 index: PollIndexOf<T, I>,
407 ) -> DispatchResult {
408 let who = ensure_signed(origin)?;
409 let target = T::Lookup::lookup(target)?;
410 let scope = if target == who { UnvoteScope::Any } else { UnvoteScope::OnlyExpired };
411 Self::try_remove_vote(&target, index, Some(class), scope)?;
412 Ok(())
413 }
414 }
415}
416
417impl<T: Config<I>, I: 'static> Pallet<T, I> {
418 fn try_vote(
420 who: &T::AccountId,
421 poll_index: PollIndexOf<T, I>,
422 vote: AccountVote<BalanceOf<T, I>>,
423 ) -> DispatchResult {
424 ensure!(
425 vote.balance() <= T::Currency::total_balance(who),
426 Error::<T, I>::InsufficientFunds
427 );
428 T::VotingHooks::on_before_vote(who, poll_index, vote)?;
430
431 T::Polls::try_access_poll(poll_index, |poll_status| {
432 let (tally, class) = poll_status.ensure_ongoing().ok_or(Error::<T, I>::NotOngoing)?;
433 VotingFor::<T, I>::try_mutate(who, &class, |voting| {
434 if let Voting::Casting(Casting { ref mut votes, delegations, .. }) = voting {
435 match votes.binary_search_by_key(&poll_index, |i| i.0) {
436 Ok(i) => {
437 tally.remove(votes[i].1).ok_or(ArithmeticError::Underflow)?;
439 if let Some(approve) = votes[i].1.as_standard() {
440 tally.reduce(approve, *delegations);
441 }
442 votes[i].1 = vote;
443 },
444 Err(i) => {
445 votes
446 .try_insert(i, (poll_index, vote))
447 .map_err(|_| Error::<T, I>::MaxVotesReached)?;
448 },
449 }
450 tally.add(vote).ok_or(ArithmeticError::Overflow)?;
452 if let Some(approve) = vote.as_standard() {
453 tally.increase(approve, *delegations);
454 }
455 } else {
456 return Err(Error::<T, I>::AlreadyDelegating.into());
457 }
458 Self::extend_lock(who, &class, vote.balance());
461 Self::deposit_event(Event::Voted { who: who.clone(), vote });
462 Ok(())
463 })
464 })
465 }
466
467 fn try_remove_vote(
474 who: &T::AccountId,
475 poll_index: PollIndexOf<T, I>,
476 class_hint: Option<ClassOf<T, I>>,
477 scope: UnvoteScope,
478 ) -> DispatchResult {
479 let class = class_hint
480 .or_else(|| Some(T::Polls::as_ongoing(poll_index)?.1))
481 .ok_or(Error::<T, I>::ClassNeeded)?;
482 VotingFor::<T, I>::try_mutate(who, class, |voting| {
483 if let Voting::Casting(Casting { ref mut votes, delegations, ref mut prior }) = voting {
484 let i = votes
485 .binary_search_by_key(&poll_index, |i| i.0)
486 .map_err(|_| Error::<T, I>::NotVoter)?;
487 let v = votes.remove(i);
488
489 T::Polls::try_access_poll(poll_index, |poll_status| match poll_status {
490 PollStatus::Ongoing(tally, _) => {
491 ensure!(matches!(scope, UnvoteScope::Any), Error::<T, I>::NoPermission);
492 tally.remove(v.1).ok_or(ArithmeticError::Underflow)?;
494 if let Some(approve) = v.1.as_standard() {
495 tally.reduce(approve, *delegations);
496 }
497 Self::deposit_event(Event::VoteRemoved { who: who.clone(), vote: v.1 });
498 T::VotingHooks::on_remove_vote(who, poll_index, Status::Ongoing);
499 Ok(())
500 },
501 PollStatus::Completed(end, approved) => {
502 if let Some((lock_periods, balance)) =
503 v.1.locked_if(vote::LockedIf::Status(approved))
504 {
505 let unlock_at = end.saturating_add(
506 T::VoteLockingPeriod::get().saturating_mul(lock_periods.into()),
507 );
508 let now = T::BlockNumberProvider::current_block_number();
509 if now < unlock_at {
510 ensure!(
511 matches!(scope, UnvoteScope::Any),
512 Error::<T, I>::NoPermissionYet
513 );
514 prior.accumulate(unlock_at, balance)
515 }
516 } else if v.1.as_standard().is_some_and(|vote| vote != approved) {
517 if let Some(to_lock) =
520 T::VotingHooks::lock_balance_on_unsuccessful_vote(who, poll_index)
521 {
522 if let AccountVote::Standard { vote, .. } = v.1 {
523 let unlock_at = end.saturating_add(
524 T::VoteLockingPeriod::get()
525 .saturating_mul(vote.conviction.lock_periods().into()),
526 );
527 let now = T::BlockNumberProvider::current_block_number();
528 if now < unlock_at {
529 ensure!(
530 matches!(scope, UnvoteScope::Any),
531 Error::<T, I>::NoPermissionYet
532 );
533 prior.accumulate(unlock_at, to_lock)
534 }
535 }
536 }
537 }
538 T::VotingHooks::on_remove_vote(who, poll_index, Status::Completed);
540 Ok(())
541 },
542 PollStatus::None => {
543 T::VotingHooks::on_remove_vote(who, poll_index, Status::None);
545 Ok(())
546 },
547 })
548 } else {
549 Ok(())
550 }
551 })
552 }
553
554 fn increase_upstream_delegation(
556 who: &T::AccountId,
557 class: &ClassOf<T, I>,
558 amount: Delegations<BalanceOf<T, I>>,
559 ) -> u32 {
560 VotingFor::<T, I>::mutate(who, class, |voting| match voting {
561 Voting::Delegating(Delegating { delegations, .. }) => {
562 *delegations = delegations.saturating_add(amount);
564 1
565 },
566 Voting::Casting(Casting { votes, delegations, .. }) => {
567 *delegations = delegations.saturating_add(amount);
568 for &(poll_index, account_vote) in votes.iter() {
569 if let AccountVote::Standard { vote, .. } = account_vote {
570 T::Polls::access_poll(poll_index, |poll_status| {
571 if let PollStatus::Ongoing(tally, _) = poll_status {
572 tally.increase(vote.aye, amount);
573 }
574 });
575 }
576 }
577 votes.len() as u32
578 },
579 })
580 }
581
582 fn reduce_upstream_delegation(
584 who: &T::AccountId,
585 class: &ClassOf<T, I>,
586 amount: Delegations<BalanceOf<T, I>>,
587 ) -> u32 {
588 VotingFor::<T, I>::mutate(who, class, |voting| match voting {
589 Voting::Delegating(Delegating { delegations, .. }) => {
590 *delegations = delegations.saturating_sub(amount);
592 1
593 },
594 Voting::Casting(Casting { votes, delegations, .. }) => {
595 *delegations = delegations.saturating_sub(amount);
596 for &(poll_index, account_vote) in votes.iter() {
597 if let AccountVote::Standard { vote, .. } = account_vote {
598 T::Polls::access_poll(poll_index, |poll_status| {
599 if let PollStatus::Ongoing(tally, _) = poll_status {
600 tally.reduce(vote.aye, amount);
601 }
602 });
603 }
604 }
605 votes.len() as u32
606 },
607 })
608 }
609
610 fn try_delegate(
614 who: T::AccountId,
615 class: ClassOf<T, I>,
616 target: T::AccountId,
617 conviction: Conviction,
618 balance: BalanceOf<T, I>,
619 ) -> Result<u32, DispatchError> {
620 ensure!(who != target, Error::<T, I>::Nonsense);
621 T::Polls::classes().binary_search(&class).map_err(|_| Error::<T, I>::BadClass)?;
622 ensure!(balance <= T::Currency::total_balance(&who), Error::<T, I>::InsufficientFunds);
623 let votes =
624 VotingFor::<T, I>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
625 let old = core::mem::replace(
626 voting,
627 Voting::Delegating(Delegating {
628 balance,
629 target: target.clone(),
630 conviction,
631 delegations: Default::default(),
632 prior: Default::default(),
633 }),
634 );
635 match old {
636 Voting::Delegating(Delegating { .. }) =>
637 return Err(Error::<T, I>::AlreadyDelegating.into()),
638 Voting::Casting(Casting { votes, delegations, prior }) => {
639 ensure!(votes.is_empty(), Error::<T, I>::AlreadyVoting);
641 voting.set_common(delegations, prior);
642 },
643 }
644
645 let votes =
646 Self::increase_upstream_delegation(&target, &class, conviction.votes(balance));
647 Self::extend_lock(&who, &class, balance);
650 Ok(votes)
651 })?;
652 Self::deposit_event(Event::<T, I>::Delegated(who, target));
653 Ok(votes)
654 }
655
656 fn try_undelegate(who: T::AccountId, class: ClassOf<T, I>) -> Result<u32, DispatchError> {
660 let votes =
661 VotingFor::<T, I>::try_mutate(&who, &class, |voting| -> Result<u32, DispatchError> {
662 match core::mem::replace(voting, Voting::default()) {
663 Voting::Delegating(Delegating {
664 balance,
665 target,
666 conviction,
667 delegations,
668 mut prior,
669 }) => {
670 let votes = Self::reduce_upstream_delegation(
672 &target,
673 &class,
674 conviction.votes(balance),
675 );
676 let now = T::BlockNumberProvider::current_block_number();
677 let lock_periods = conviction.lock_periods().into();
678 prior.accumulate(
679 now.saturating_add(
680 T::VoteLockingPeriod::get().saturating_mul(lock_periods),
681 ),
682 balance,
683 );
684 voting.set_common(delegations, prior);
685
686 Ok(votes)
687 },
688 Voting::Casting(_) => Err(Error::<T, I>::NotDelegating.into()),
689 }
690 })?;
691 Self::deposit_event(Event::<T, I>::Undelegated(who));
692 Ok(votes)
693 }
694
695 fn extend_lock(who: &T::AccountId, class: &ClassOf<T, I>, amount: BalanceOf<T, I>) {
696 ClassLocksFor::<T, I>::mutate(who, |locks| {
697 match locks.iter().position(|x| &x.0 == class) {
698 Some(i) => locks[i].1 = locks[i].1.max(amount),
699 None => {
700 let ok = locks.try_push((class.clone(), amount)).is_ok();
701 debug_assert!(
702 ok,
703 "Vec bounded by number of classes; \
704 all items in Vec associated with a unique class; \
705 qed"
706 );
707 },
708 }
709 });
710 T::Currency::extend_lock(
711 CONVICTION_VOTING_ID,
712 who,
713 amount,
714 WithdrawReasons::except(WithdrawReasons::RESERVE),
715 );
716 }
717
718 fn update_lock(class: &ClassOf<T, I>, who: &T::AccountId) {
721 let class_lock_needed = VotingFor::<T, I>::mutate(who, class, |voting| {
722 voting.rejig(T::BlockNumberProvider::current_block_number());
723 voting.locked_balance()
724 });
725 let lock_needed = ClassLocksFor::<T, I>::mutate(who, |locks| {
726 locks.retain(|x| &x.0 != class);
727 if !class_lock_needed.is_zero() {
728 let ok = locks.try_push((class.clone(), class_lock_needed)).is_ok();
729 debug_assert!(
730 ok,
731 "Vec bounded by number of classes; \
732 all items in Vec associated with a unique class; \
733 qed"
734 );
735 }
736 locks.iter().map(|x| x.1).max().unwrap_or(Zero::zero())
737 });
738 if lock_needed.is_zero() {
739 T::Currency::remove_lock(CONVICTION_VOTING_ID, who);
740 } else {
741 T::Currency::set_lock(
742 CONVICTION_VOTING_ID,
743 who,
744 lock_needed,
745 WithdrawReasons::except(WithdrawReasons::RESERVE),
746 );
747 }
748 }
749}