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(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug, TypeInfo, MaxEncodedLen)]
108#[scale_info(skip_type_params(MaxApprovals))]
109pub struct Multisig<BlockNumber, Balance, AccountId, MaxApprovals>
110where
111 MaxApprovals: Get<u32>,
112{
113 pub when: Timepoint<BlockNumber>,
115 pub deposit: Balance,
117 pub depositor: AccountId,
119 pub approvals: BoundedVec<AccountId, MaxApprovals>,
121}
122
123type CallHash = [u8; 32];
124
125enum CallOrHash<T: Config> {
126 Call(<T as Config>::RuntimeCall),
127 Hash([u8; 32]),
128}
129
130#[frame::pallet]
131pub mod pallet {
132 use super::*;
133
134 #[pallet::config]
135 pub trait Config: frame_system::Config {
136 #[allow(deprecated)]
138 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
139
140 type RuntimeCall: Parameter
142 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
143 + GetDispatchInfo
144 + From<frame_system::Call<Self>>;
145
146 type Currency: ReservableCurrency<Self::AccountId>;
148
149 #[pallet::constant]
156 type DepositBase: Get<BalanceOf<Self>>;
157
158 #[pallet::constant]
162 type DepositFactor: Get<BalanceOf<Self>>;
163
164 #[pallet::constant]
166 type MaxSignatories: Get<u32>;
167
168 type WeightInfo: weights::WeightInfo;
170
171 type BlockNumberProvider: BlockNumberProvider;
194 }
195
196 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
198
199 #[pallet::pallet]
200 #[pallet::storage_version(STORAGE_VERSION)]
201 pub struct Pallet<T>(_);
202
203 #[pallet::storage]
205 pub type Multisigs<T: Config> = StorageDoubleMap<
206 _,
207 Twox64Concat,
208 T::AccountId,
209 Blake2_128Concat,
210 [u8; 32],
211 Multisig<BlockNumberFor<T>, BalanceOf<T>, T::AccountId, T::MaxSignatories>,
212 >;
213
214 #[pallet::error]
215 pub enum Error<T> {
216 MinimumThreshold,
218 AlreadyApproved,
220 NoApprovalsNeeded,
222 TooFewSignatories,
224 TooManySignatories,
226 SignatoriesOutOfOrder,
228 SenderInSignatories,
230 NotFound,
232 NotOwner,
235 NoTimepoint,
237 WrongTimepoint,
239 UnexpectedTimepoint,
241 MaxWeightTooLow,
243 AlreadyStored,
245 }
246
247 #[pallet::event]
248 #[pallet::generate_deposit(pub(super) fn deposit_event)]
249 pub enum Event<T: Config> {
250 NewMultisig { approving: T::AccountId, multisig: T::AccountId, call_hash: CallHash },
252 MultisigApproval {
254 approving: T::AccountId,
255 timepoint: Timepoint<BlockNumberFor<T>>,
256 multisig: T::AccountId,
257 call_hash: CallHash,
258 },
259 MultisigExecuted {
261 approving: T::AccountId,
262 timepoint: Timepoint<BlockNumberFor<T>>,
263 multisig: T::AccountId,
264 call_hash: CallHash,
265 result: DispatchResult,
266 },
267 MultisigCancelled {
269 cancelling: T::AccountId,
270 timepoint: Timepoint<BlockNumberFor<T>>,
271 multisig: T::AccountId,
272 call_hash: CallHash,
273 },
274 DepositPoked {
276 who: T::AccountId,
277 call_hash: CallHash,
278 old_deposit: BalanceOf<T>,
279 new_deposit: BalanceOf<T>,
280 },
281 }
282
283 #[pallet::hooks]
284 impl<T: Config> Hooks<frame_system::pallet_prelude::BlockNumberFor<T>> for Pallet<T> {}
285
286 #[pallet::call]
287 impl<T: Config> Pallet<T> {
288 #[pallet::call_index(0)]
301 #[pallet::weight({
302 let dispatch_info = call.get_dispatch_info();
303 (
304 T::WeightInfo::as_multi_threshold_1(call.using_encoded(|c| c.len() as u32))
305 .saturating_add(T::DbWeight::get().reads_writes(1, 1))
307 .saturating_add(dispatch_info.call_weight),
308 dispatch_info.class,
309 )
310 })]
311 pub fn as_multi_threshold_1(
312 origin: OriginFor<T>,
313 other_signatories: Vec<T::AccountId>,
314 call: Box<<T as Config>::RuntimeCall>,
315 ) -> DispatchResultWithPostInfo {
316 let who = ensure_signed(origin)?;
317 let max_sigs = T::MaxSignatories::get() as usize;
318 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
319 let other_signatories_len = other_signatories.len();
320 ensure!(other_signatories_len < max_sigs, Error::<T>::TooManySignatories);
321 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
322
323 let id = Self::multi_account_id(&signatories, 1);
324
325 let (call_len, call_hash) = call.using_encoded(|c| (c.len(), blake2_256(&c)));
326 let result = call.dispatch(RawOrigin::Signed(id.clone()).into());
327
328 Self::deposit_event(Event::MultisigExecuted {
329 approving: who,
330 timepoint: Self::timepoint(),
331 multisig: id,
332 call_hash,
333 result: result.map(|_| ()).map_err(|e| e.error),
334 });
335
336 result
337 .map(|post_dispatch_info| {
338 post_dispatch_info
339 .actual_weight
340 .map(|actual_weight| {
341 T::WeightInfo::as_multi_threshold_1(call_len as u32)
342 .saturating_add(actual_weight)
343 })
344 .into()
345 })
346 .map_err(|err| match err.post_info.actual_weight {
347 Some(actual_weight) => {
348 let weight_used = T::WeightInfo::as_multi_threshold_1(call_len as u32)
349 .saturating_add(actual_weight);
350 let post_info = Some(weight_used).into();
351 DispatchErrorWithPostInfo { post_info, error: err.error }
352 },
353 None => err,
354 })
355 }
356
357 #[pallet::call_index(1)]
397 #[pallet::weight({
398 let s = other_signatories.len() as u32;
399 let z = call.using_encoded(|d| d.len()) as u32;
400
401 T::WeightInfo::as_multi_create(s, z)
402 .max(T::WeightInfo::as_multi_approve(s, z))
403 .max(T::WeightInfo::as_multi_complete(s, z))
404 .saturating_add(*max_weight)
405 })]
406 pub fn as_multi(
407 origin: OriginFor<T>,
408 threshold: u16,
409 other_signatories: Vec<T::AccountId>,
410 maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
411 call: Box<<T as Config>::RuntimeCall>,
412 max_weight: Weight,
413 ) -> DispatchResultWithPostInfo {
414 let who = ensure_signed(origin)?;
415 Self::operate(
416 who,
417 threshold,
418 other_signatories,
419 maybe_timepoint,
420 CallOrHash::Call(*call),
421 max_weight,
422 )
423 }
424
425 #[pallet::call_index(2)]
456 #[pallet::weight({
457 let s = other_signatories.len() as u32;
458
459 T::WeightInfo::approve_as_multi_create(s)
460 .max(T::WeightInfo::approve_as_multi_approve(s))
461 .saturating_add(*max_weight)
462 })]
463 pub fn approve_as_multi(
464 origin: OriginFor<T>,
465 threshold: u16,
466 other_signatories: Vec<T::AccountId>,
467 maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
468 call_hash: [u8; 32],
469 max_weight: Weight,
470 ) -> DispatchResultWithPostInfo {
471 let who = ensure_signed(origin)?;
472 Self::operate(
473 who,
474 threshold,
475 other_signatories,
476 maybe_timepoint,
477 CallOrHash::Hash(call_hash),
478 max_weight,
479 )
480 }
481
482 #[pallet::call_index(3)]
504 #[pallet::weight(T::WeightInfo::cancel_as_multi(other_signatories.len() as u32))]
505 pub fn cancel_as_multi(
506 origin: OriginFor<T>,
507 threshold: u16,
508 other_signatories: Vec<T::AccountId>,
509 timepoint: Timepoint<BlockNumberFor<T>>,
510 call_hash: [u8; 32],
511 ) -> DispatchResult {
512 let who = ensure_signed(origin)?;
513 ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
514 let max_sigs = T::MaxSignatories::get() as usize;
515 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
516 ensure!(other_signatories.len() < max_sigs, Error::<T>::TooManySignatories);
517 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
518
519 let id = Self::multi_account_id(&signatories, threshold);
520
521 let m = <Multisigs<T>>::get(&id, call_hash).ok_or(Error::<T>::NotFound)?;
522 ensure!(m.when == timepoint, Error::<T>::WrongTimepoint);
523 ensure!(m.depositor == who, Error::<T>::NotOwner);
524
525 let err_amount = T::Currency::unreserve(&m.depositor, m.deposit);
526 debug_assert!(err_amount.is_zero());
527 <Multisigs<T>>::remove(&id, &call_hash);
528
529 Self::deposit_event(Event::MultisigCancelled {
530 cancelling: who,
531 timepoint,
532 multisig: id,
533 call_hash,
534 });
535 Ok(())
536 }
537
538 #[pallet::call_index(4)]
552 #[pallet::weight(T::WeightInfo::poke_deposit(other_signatories.len() as u32))]
553 pub fn poke_deposit(
554 origin: OriginFor<T>,
555 threshold: u16,
556 other_signatories: Vec<T::AccountId>,
557 call_hash: [u8; 32],
558 ) -> DispatchResultWithPostInfo {
559 let who = ensure_signed(origin)?;
560 ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
561 let max_sigs = T::MaxSignatories::get() as usize;
562 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
563 ensure!(other_signatories.len() < max_sigs, Error::<T>::TooManySignatories);
564 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
566 let id = Self::multi_account_id(&signatories, threshold);
567
568 Multisigs::<T>::try_mutate(
569 &id,
570 call_hash,
571 |maybe_multisig| -> DispatchResultWithPostInfo {
572 let mut multisig = maybe_multisig.take().ok_or(Error::<T>::NotFound)?;
573 ensure!(multisig.depositor == who, Error::<T>::NotOwner);
574
575 let new_deposit = Self::deposit(threshold);
577 let old_deposit = multisig.deposit;
578
579 if new_deposit == old_deposit {
580 *maybe_multisig = Some(multisig);
581 return Ok(Pays::Yes.into());
582 }
583
584 if new_deposit > old_deposit {
586 let extra = new_deposit.saturating_sub(old_deposit);
587 T::Currency::reserve(&who, extra)?;
588 } else {
589 let excess = old_deposit.saturating_sub(new_deposit);
590 let remaining_unreserved = T::Currency::unreserve(&who, excess);
591 if !remaining_unreserved.is_zero() {
592 defensive!(
593 "Failed to unreserve for full amount for multisig. (Call Hash, Requested, Actual): ",
594 (call_hash, excess, excess.saturating_sub(remaining_unreserved))
595 );
596 }
597 }
598
599 multisig.deposit = new_deposit;
601 *maybe_multisig = Some(multisig);
602
603 Self::deposit_event(Event::DepositPoked {
605 who: who.clone(),
606 call_hash,
607 old_deposit,
608 new_deposit,
609 });
610
611 Ok(Pays::No.into())
612 },
613 )
614 }
615 }
616}
617
618impl<T: Config> Pallet<T> {
619 pub fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId {
624 let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(blake2_256);
625 Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
626 .expect("infinite length input; no invalid inputs for type; qed")
627 }
628
629 fn operate(
630 who: T::AccountId,
631 threshold: u16,
632 other_signatories: Vec<T::AccountId>,
633 maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
634 call_or_hash: CallOrHash<T>,
635 max_weight: Weight,
636 ) -> DispatchResultWithPostInfo {
637 ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
638 let max_sigs = T::MaxSignatories::get() as usize;
639 ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
640 let other_signatories_len = other_signatories.len();
641 ensure!(other_signatories_len < max_sigs, Error::<T>::TooManySignatories);
642 let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
643
644 let id = Self::multi_account_id(&signatories, threshold);
645
646 let (call_hash, call_len, maybe_call) = match call_or_hash {
648 CallOrHash::Call(call) => {
649 let (call_hash, call_len) = call.using_encoded(|d| (blake2_256(d), d.len()));
650 (call_hash, call_len, Some(call))
651 },
652 CallOrHash::Hash(h) => (h, 0, None),
653 };
654
655 if let Some(mut m) = <Multisigs<T>>::get(&id, call_hash) {
657 let timepoint = maybe_timepoint.ok_or(Error::<T>::NoTimepoint)?;
659 ensure!(m.when == timepoint, Error::<T>::WrongTimepoint);
660
661 let mut approvals = m.approvals.len() as u16;
663 let maybe_pos = m.approvals.binary_search(&who).err().filter(|_| approvals < threshold);
665 if maybe_pos.is_some() {
667 approvals += 1;
668 }
669
670 if let Some(call) = maybe_call.filter(|_| approvals >= threshold) {
672 ensure!(
674 call.get_dispatch_info().call_weight.all_lte(max_weight),
675 Error::<T>::MaxWeightTooLow
676 );
677
678 <Multisigs<T>>::remove(&id, call_hash);
681 T::Currency::unreserve(&m.depositor, m.deposit);
682
683 let result = call.dispatch(RawOrigin::Signed(id.clone()).into());
684 Self::deposit_event(Event::MultisigExecuted {
685 approving: who,
686 timepoint,
687 multisig: id,
688 call_hash,
689 result: result.map(|_| ()).map_err(|e| e.error),
690 });
691 Ok(get_result_weight(result)
692 .map(|actual_weight| {
693 T::WeightInfo::as_multi_complete(
694 other_signatories_len as u32,
695 call_len as u32,
696 )
697 .saturating_add(actual_weight)
698 })
699 .into())
700 } else {
701 if let Some(pos) = maybe_pos {
705 m.approvals
707 .try_insert(pos, who.clone())
708 .map_err(|_| Error::<T>::TooManySignatories)?;
709 <Multisigs<T>>::insert(&id, call_hash, m);
710 Self::deposit_event(Event::MultisigApproval {
711 approving: who,
712 timepoint,
713 multisig: id,
714 call_hash,
715 });
716 } else {
717 Err(Error::<T>::AlreadyApproved)?
720 }
721
722 let final_weight =
723 T::WeightInfo::as_multi_approve(other_signatories_len as u32, call_len as u32);
724 Ok(Some(final_weight).into())
726 }
727 } else {
728 ensure!(maybe_timepoint.is_none(), Error::<T>::UnexpectedTimepoint);
730
731 let deposit = Self::deposit(threshold);
733
734 T::Currency::reserve(&who, deposit)?;
735
736 let initial_approvals =
737 vec![who.clone()].try_into().map_err(|_| Error::<T>::TooManySignatories)?;
738
739 <Multisigs<T>>::insert(
740 &id,
741 call_hash,
742 Multisig {
743 when: Self::timepoint(),
744 deposit,
745 depositor: who.clone(),
746 approvals: initial_approvals,
747 },
748 );
749 Self::deposit_event(Event::NewMultisig { approving: who, multisig: id, call_hash });
750
751 let final_weight =
752 T::WeightInfo::as_multi_create(other_signatories_len as u32, call_len as u32);
753 Ok(Some(final_weight).into())
755 }
756 }
757
758 pub fn timepoint() -> Timepoint<BlockNumberFor<T>> {
760 Timepoint {
761 height: T::BlockNumberProvider::current_block_number(),
762 index: <frame_system::Pallet<T>>::extrinsic_index().unwrap_or_default(),
763 }
764 }
765
766 fn ensure_sorted_and_insert(
768 other_signatories: Vec<T::AccountId>,
769 who: T::AccountId,
770 ) -> Result<Vec<T::AccountId>, DispatchError> {
771 let mut signatories = other_signatories;
772 let mut maybe_last = None;
773 let mut index = 0;
774 for item in signatories.iter() {
775 if let Some(last) = maybe_last {
776 ensure!(last < item, Error::<T>::SignatoriesOutOfOrder);
777 }
778 if item <= &who {
779 ensure!(item != &who, Error::<T>::SenderInSignatories);
780 index += 1;
781 }
782 maybe_last = Some(item);
783 }
784 signatories.insert(index, who);
785 Ok(signatories)
786 }
787
788 pub fn deposit(threshold: u16) -> BalanceOf<T> {
792 T::DepositBase::get() + T::DepositFactor::get() * threshold.into()
793 }
794}
795
796fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
800 match result {
801 Ok(post_info) => post_info.actual_weight,
802 Err(err) => err.post_info.actual_weight,
803 }
804}