1#![cfg_attr(not(feature = "std"), no_std)]
45
46mod benchmarking;
47pub mod migrations;
48mod tests;
49pub mod weights;
50
51extern crate alloc;
52use alloc::{boxed::Box, vec, vec::Vec};
53use frame::{
54 prelude::*,
55 traits::{Currency, ReservableCurrency},
56};
57use frame_system::RawOrigin;
58pub use weights::WeightInfo;
59
60pub use pallet::*;
62
63pub const LOG_TARGET: &'static str = "runtime::multisig";
65
66#[macro_export]
68macro_rules! log {
69 ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
70 log::$level!(
71 target: crate::LOG_TARGET,
72 concat!("[{:?}] ✍️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
73 )
74 };
75}
76
77pub type BalanceOf<T> =
78 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
79
80pub type BlockNumberFor<T> =
81 <<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
82
83#[derive(
87 Copy,
88 Clone,
89 Eq,
90 PartialEq,
91 Encode,
92 Decode,
93 DecodeWithMemTracking,
94 Default,
95 RuntimeDebug,
96 TypeInfo,
97 MaxEncodedLen,
98)]
99pub struct Timepoint<BlockNumber> {
100 pub height: BlockNumber,
102 pub index: u32,
104}
105
106#[derive(
108 Clone,
109 Eq,
110 PartialEq,
111 Encode,
112 Decode,
113 Default,
114 RuntimeDebug,
115 TypeInfo,
116 MaxEncodedLen,
117 DecodeWithMemTracking,
118)]
119#[scale_info(skip_type_params(MaxApprovals))]
120pub struct Multisig<BlockNumber, Balance, AccountId, MaxApprovals>
121where
122 MaxApprovals: Get<u32>,
123{
124 pub when: Timepoint<BlockNumber>,
126 pub deposit: Balance,
128 pub depositor: AccountId,
130 pub approvals: BoundedVec<AccountId, MaxApprovals>,
132}
133
134type CallHash = [u8; 32];
135
136enum CallOrHash<T: Config> {
137 Call(<T as Config>::RuntimeCall),
138 Hash([u8; 32]),
139}
140
141#[frame::pallet]
142pub mod pallet {
143 use super::*;
144
145 #[pallet::config]
146 pub trait Config: frame_system::Config {
147 #[allow(deprecated)]
149 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
150
151 type RuntimeCall: Parameter
153 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
154 + GetDispatchInfo
155 + From<frame_system::Call<Self>>;
156
157 type Currency: ReservableCurrency<Self::AccountId>;
159
160 #[pallet::constant]
167 type DepositBase: Get<BalanceOf<Self>>;
168
169 #[pallet::constant]
173 type DepositFactor: Get<BalanceOf<Self>>;
174
175 #[pallet::constant]
177 type MaxSignatories: Get<u32>;
178
179 type WeightInfo: weights::WeightInfo;
181
182 type BlockNumberProvider: BlockNumberProvider;
205 }
206
207 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
209
210 #[pallet::pallet]
211 #[pallet::storage_version(STORAGE_VERSION)]
212 pub struct Pallet<T>(_);
213
214 #[pallet::storage]
216 pub type Multisigs<T: Config> = StorageDoubleMap<
217 _,
218 Twox64Concat,
219 T::AccountId,
220 Blake2_128Concat,
221 [u8; 32],
222 Multisig<BlockNumberFor<T>, BalanceOf<T>, T::AccountId, T::MaxSignatories>,
223 >;
224
225 #[pallet::error]
226 pub enum Error<T> {
227 MinimumThreshold,
229 AlreadyApproved,
231 NoApprovalsNeeded,
233 TooFewSignatories,
235 TooManySignatories,
237 SignatoriesOutOfOrder,
239 SenderInSignatories,
241 NotFound,
243 NotOwner,
246 NoTimepoint,
248 WrongTimepoint,
250 UnexpectedTimepoint,
252 MaxWeightTooLow,
254 AlreadyStored,
256 }
257
258 #[pallet::event]
259 #[pallet::generate_deposit(pub(super) fn deposit_event)]
260 pub enum Event<T: Config> {
261 NewMultisig { approving: T::AccountId, multisig: T::AccountId, call_hash: CallHash },
263 MultisigApproval {
265 approving: T::AccountId,
266 timepoint: Timepoint<BlockNumberFor<T>>,
267 multisig: T::AccountId,
268 call_hash: CallHash,
269 },
270 MultisigExecuted {
272 approving: T::AccountId,
273 timepoint: Timepoint<BlockNumberFor<T>>,
274 multisig: T::AccountId,
275 call_hash: CallHash,
276 result: DispatchResult,
277 },
278 MultisigCancelled {
280 cancelling: T::AccountId,
281 timepoint: Timepoint<BlockNumberFor<T>>,
282 multisig: T::AccountId,
283 call_hash: CallHash,
284 },
285 DepositPoked {
287 who: T::AccountId,
288 call_hash: CallHash,
289 old_deposit: BalanceOf<T>,
290 new_deposit: BalanceOf<T>,
291 },
292 }
293
294 #[pallet::hooks]
295 impl<T: Config> Hooks<frame_system::pallet_prelude::BlockNumberFor<T>> for Pallet<T> {}
296
297 #[pallet::call]
298 impl<T: Config> Pallet<T> {
299 #[pallet::call_index(0)]
312 #[pallet::weight({
313 let dispatch_info = call.get_dispatch_info();
314 (
315 T::WeightInfo::as_multi_threshold_1(call.using_encoded(|c| c.len() as u32))
316 .saturating_add(T::DbWeight::get().reads_writes(1, 1))
318 .saturating_add(dispatch_info.call_weight),
319 dispatch_info.class,
320 )
321 })]
322 pub fn as_multi_threshold_1(
323 origin: OriginFor<T>,
324 other_signatories: Vec<T::AccountId>,
325 call: Box<<T as Config>::RuntimeCall>,
326 ) -> DispatchResultWithPostInfo {
327 let who = ensure_signed(origin)?;
328 let max_sigs = T::MaxSignatories::get() as usize;
329 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
330 let other_signatories_len = other_signatories.len();
331 ensure!(other_signatories_len < max_sigs, Error::<T>::TooManySignatories);
332 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
333
334 let id = Self::multi_account_id(&signatories, 1);
335
336 let (call_len, call_hash) = call.using_encoded(|c| (c.len(), blake2_256(&c)));
337 let result = call.dispatch(RawOrigin::Signed(id.clone()).into());
338
339 Self::deposit_event(Event::MultisigExecuted {
340 approving: who,
341 timepoint: Self::timepoint(),
342 multisig: id,
343 call_hash,
344 result: result.map(|_| ()).map_err(|e| e.error),
345 });
346
347 result
348 .map(|post_dispatch_info| {
349 post_dispatch_info
350 .actual_weight
351 .map(|actual_weight| {
352 T::WeightInfo::as_multi_threshold_1(call_len as u32)
353 .saturating_add(actual_weight)
354 })
355 .into()
356 })
357 .map_err(|err| match err.post_info.actual_weight {
358 Some(actual_weight) => {
359 let weight_used = T::WeightInfo::as_multi_threshold_1(call_len as u32)
360 .saturating_add(actual_weight);
361 let post_info = Some(weight_used).into();
362 DispatchErrorWithPostInfo { post_info, error: err.error }
363 },
364 None => err,
365 })
366 }
367
368 #[pallet::call_index(1)]
408 #[pallet::weight({
409 let s = other_signatories.len() as u32;
410 let z = call.using_encoded(|d| d.len()) as u32;
411
412 T::WeightInfo::as_multi_create(s, z)
413 .max(T::WeightInfo::as_multi_approve(s, z))
414 .max(T::WeightInfo::as_multi_complete(s, z))
415 .saturating_add(*max_weight)
416 })]
417 pub fn as_multi(
418 origin: OriginFor<T>,
419 threshold: u16,
420 other_signatories: Vec<T::AccountId>,
421 maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
422 call: Box<<T as Config>::RuntimeCall>,
423 max_weight: Weight,
424 ) -> DispatchResultWithPostInfo {
425 let who = ensure_signed(origin)?;
426 Self::operate(
427 who,
428 threshold,
429 other_signatories,
430 maybe_timepoint,
431 CallOrHash::Call(*call),
432 max_weight,
433 )
434 }
435
436 #[pallet::call_index(2)]
467 #[pallet::weight({
468 let s = other_signatories.len() as u32;
469
470 T::WeightInfo::approve_as_multi_create(s)
471 .max(T::WeightInfo::approve_as_multi_approve(s))
472 .saturating_add(*max_weight)
473 })]
474 pub fn approve_as_multi(
475 origin: OriginFor<T>,
476 threshold: u16,
477 other_signatories: Vec<T::AccountId>,
478 maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
479 call_hash: [u8; 32],
480 max_weight: Weight,
481 ) -> DispatchResultWithPostInfo {
482 let who = ensure_signed(origin)?;
483 Self::operate(
484 who,
485 threshold,
486 other_signatories,
487 maybe_timepoint,
488 CallOrHash::Hash(call_hash),
489 max_weight,
490 )
491 }
492
493 #[pallet::call_index(3)]
515 #[pallet::weight(T::WeightInfo::cancel_as_multi(other_signatories.len() as u32))]
516 pub fn cancel_as_multi(
517 origin: OriginFor<T>,
518 threshold: u16,
519 other_signatories: Vec<T::AccountId>,
520 timepoint: Timepoint<BlockNumberFor<T>>,
521 call_hash: [u8; 32],
522 ) -> DispatchResult {
523 let who = ensure_signed(origin)?;
524 ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
525 let max_sigs = T::MaxSignatories::get() as usize;
526 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
527 ensure!(other_signatories.len() < max_sigs, Error::<T>::TooManySignatories);
528 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
529
530 let id = Self::multi_account_id(&signatories, threshold);
531
532 let m = <Multisigs<T>>::get(&id, call_hash).ok_or(Error::<T>::NotFound)?;
533 ensure!(m.when == timepoint, Error::<T>::WrongTimepoint);
534 ensure!(m.depositor == who, Error::<T>::NotOwner);
535
536 let err_amount = T::Currency::unreserve(&m.depositor, m.deposit);
537 debug_assert!(err_amount.is_zero());
538 <Multisigs<T>>::remove(&id, &call_hash);
539
540 Self::deposit_event(Event::MultisigCancelled {
541 cancelling: who,
542 timepoint,
543 multisig: id,
544 call_hash,
545 });
546 Ok(())
547 }
548
549 #[pallet::call_index(4)]
563 #[pallet::weight(T::WeightInfo::poke_deposit(other_signatories.len() as u32))]
564 pub fn poke_deposit(
565 origin: OriginFor<T>,
566 threshold: u16,
567 other_signatories: Vec<T::AccountId>,
568 call_hash: [u8; 32],
569 ) -> DispatchResultWithPostInfo {
570 let who = ensure_signed(origin)?;
571 ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
572 let max_sigs = T::MaxSignatories::get() as usize;
573 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
574 ensure!(other_signatories.len() < max_sigs, Error::<T>::TooManySignatories);
575 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
577 let id = Self::multi_account_id(&signatories, threshold);
578
579 Multisigs::<T>::try_mutate(
580 &id,
581 call_hash,
582 |maybe_multisig| -> DispatchResultWithPostInfo {
583 let mut multisig = maybe_multisig.take().ok_or(Error::<T>::NotFound)?;
584 ensure!(multisig.depositor == who, Error::<T>::NotOwner);
585
586 let new_deposit = Self::deposit(threshold);
588 let old_deposit = multisig.deposit;
589
590 if new_deposit == old_deposit {
591 *maybe_multisig = Some(multisig);
592 return Ok(Pays::Yes.into());
593 }
594
595 if new_deposit > old_deposit {
597 let extra = new_deposit.saturating_sub(old_deposit);
598 T::Currency::reserve(&who, extra)?;
599 } else {
600 let excess = old_deposit.saturating_sub(new_deposit);
601 let remaining_unreserved = T::Currency::unreserve(&who, excess);
602 if !remaining_unreserved.is_zero() {
603 defensive!(
604 "Failed to unreserve for full amount for multisig. (Call Hash, Requested, Actual): ",
605 (call_hash, excess, excess.saturating_sub(remaining_unreserved))
606 );
607 }
608 }
609
610 multisig.deposit = new_deposit;
612 *maybe_multisig = Some(multisig);
613
614 Self::deposit_event(Event::DepositPoked {
616 who: who.clone(),
617 call_hash,
618 old_deposit,
619 new_deposit,
620 });
621
622 Ok(Pays::No.into())
623 },
624 )
625 }
626 }
627}
628
629impl<T: Config> Pallet<T> {
630 pub fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId {
635 let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(blake2_256);
636 Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
637 .expect("infinite length input; no invalid inputs for type; qed")
638 }
639
640 fn operate(
641 who: T::AccountId,
642 threshold: u16,
643 other_signatories: Vec<T::AccountId>,
644 maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
645 call_or_hash: CallOrHash<T>,
646 max_weight: Weight,
647 ) -> DispatchResultWithPostInfo {
648 ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
649 let max_sigs = T::MaxSignatories::get() as usize;
650 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
651 let other_signatories_len = other_signatories.len();
652 ensure!(other_signatories_len < max_sigs, Error::<T>::TooManySignatories);
653 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
654
655 let id = Self::multi_account_id(&signatories, threshold);
656
657 let (call_hash, call_len, maybe_call) = match call_or_hash {
659 CallOrHash::Call(call) => {
660 let (call_hash, call_len) = call.using_encoded(|d| (blake2_256(d), d.len()));
661 (call_hash, call_len, Some(call))
662 },
663 CallOrHash::Hash(h) => (h, 0, None),
664 };
665
666 if let Some(mut m) = <Multisigs<T>>::get(&id, call_hash) {
668 let timepoint = maybe_timepoint.ok_or(Error::<T>::NoTimepoint)?;
670 ensure!(m.when == timepoint, Error::<T>::WrongTimepoint);
671
672 let mut approvals = m.approvals.len() as u16;
674 let maybe_pos = m.approvals.binary_search(&who).err().filter(|_| approvals < threshold);
676 if maybe_pos.is_some() {
678 approvals += 1;
679 }
680
681 if let Some(call) = maybe_call.filter(|_| approvals >= threshold) {
683 ensure!(
685 call.get_dispatch_info().call_weight.all_lte(max_weight),
686 Error::<T>::MaxWeightTooLow
687 );
688
689 <Multisigs<T>>::remove(&id, call_hash);
692 T::Currency::unreserve(&m.depositor, m.deposit);
693
694 let result = call.dispatch(RawOrigin::Signed(id.clone()).into());
695 Self::deposit_event(Event::MultisigExecuted {
696 approving: who,
697 timepoint,
698 multisig: id,
699 call_hash,
700 result: result.map(|_| ()).map_err(|e| e.error),
701 });
702 Ok(get_result_weight(result)
703 .map(|actual_weight| {
704 T::WeightInfo::as_multi_complete(
705 other_signatories_len as u32,
706 call_len as u32,
707 )
708 .saturating_add(actual_weight)
709 })
710 .into())
711 } else {
712 if let Some(pos) = maybe_pos {
716 m.approvals
718 .try_insert(pos, who.clone())
719 .map_err(|_| Error::<T>::TooManySignatories)?;
720 <Multisigs<T>>::insert(&id, call_hash, m);
721 Self::deposit_event(Event::MultisigApproval {
722 approving: who,
723 timepoint,
724 multisig: id,
725 call_hash,
726 });
727 } else {
728 Err(Error::<T>::AlreadyApproved)?
731 }
732
733 let final_weight =
734 T::WeightInfo::as_multi_approve(other_signatories_len as u32, call_len as u32);
735 Ok(Some(final_weight).into())
737 }
738 } else {
739 ensure!(maybe_timepoint.is_none(), Error::<T>::UnexpectedTimepoint);
741
742 let deposit = Self::deposit(threshold);
744
745 T::Currency::reserve(&who, deposit)?;
746
747 let initial_approvals =
748 vec![who.clone()].try_into().map_err(|_| Error::<T>::TooManySignatories)?;
749
750 <Multisigs<T>>::insert(
751 &id,
752 call_hash,
753 Multisig {
754 when: Self::timepoint(),
755 deposit,
756 depositor: who.clone(),
757 approvals: initial_approvals,
758 },
759 );
760 Self::deposit_event(Event::NewMultisig { approving: who, multisig: id, call_hash });
761
762 let final_weight =
763 T::WeightInfo::as_multi_create(other_signatories_len as u32, call_len as u32);
764 Ok(Some(final_weight).into())
766 }
767 }
768
769 pub fn timepoint() -> Timepoint<BlockNumberFor<T>> {
771 Timepoint {
772 height: T::BlockNumberProvider::current_block_number(),
773 index: <frame_system::Pallet<T>>::extrinsic_index().unwrap_or_default(),
774 }
775 }
776
777 fn ensure_sorted_and_insert(
779 other_signatories: Vec<T::AccountId>,
780 who: T::AccountId,
781 ) -> Result<Vec<T::AccountId>, DispatchError> {
782 let mut signatories = other_signatories;
783 let mut maybe_last = None;
784 let mut index = 0;
785 for item in signatories.iter() {
786 if let Some(last) = maybe_last {
787 ensure!(last < item, Error::<T>::SignatoriesOutOfOrder);
788 }
789 if item <= &who {
790 ensure!(item != &who, Error::<T>::SenderInSignatories);
791 index += 1;
792 }
793 maybe_last = Some(item);
794 }
795 signatories.insert(index, who);
796 Ok(signatories)
797 }
798
799 pub fn deposit(threshold: u16) -> BalanceOf<T> {
803 T::DepositBase::get() + T::DepositFactor::get() * threshold.into()
804 }
805}
806
807fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
811 match result {
812 Ok(post_info) => post_info.actual_weight,
813 Err(err) => err.post_info.actual_weight,
814 }
815}