#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
pub mod migrations;
mod tests;
pub mod weights;
extern crate alloc;
use alloc::{boxed::Box, vec, vec::Vec};
use frame::{
prelude::*,
traits::{Currency, ReservableCurrency},
};
use frame_system::RawOrigin;
pub use weights::WeightInfo;
pub use pallet::*;
pub const LOG_TARGET: &'static str = "runtime::multisig";
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: crate::LOG_TARGET,
concat!("[{:?}] ✍️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
)
};
}
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type BlockNumberFor<T> =
<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Encode,
Decode,
DecodeWithMemTracking,
Default,
Debug,
TypeInfo,
MaxEncodedLen,
)]
pub struct Timepoint<BlockNumber> {
pub height: BlockNumber,
pub index: u32,
}
#[derive(
Clone,
Eq,
PartialEq,
Encode,
Decode,
Default,
Debug,
TypeInfo,
MaxEncodedLen,
DecodeWithMemTracking,
)]
#[scale_info(skip_type_params(MaxApprovals))]
pub struct Multisig<BlockNumber, Balance, AccountId, MaxApprovals>
where
MaxApprovals: Get<u32>,
{
pub when: Timepoint<BlockNumber>,
pub deposit: Balance,
pub depositor: AccountId,
pub approvals: BoundedVec<AccountId, MaxApprovals>,
}
type CallHash = [u8; 32];
enum CallOrHash<T: Config> {
Call(<T as Config>::RuntimeCall),
Hash([u8; 32]),
}
#[frame::pallet]
pub mod pallet {
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config {
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type RuntimeCall: Parameter
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
+ GetDispatchInfo
+ From<frame_system::Call<Self>>;
type Currency: ReservableCurrency<Self::AccountId>;
#[pallet::constant]
type DepositBase: Get<BalanceOf<Self>>;
#[pallet::constant]
type DepositFactor: Get<BalanceOf<Self>>;
#[pallet::constant]
type MaxSignatories: Get<u32>;
type WeightInfo: weights::WeightInfo;
type BlockNumberProvider: BlockNumberProvider;
}
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::storage]
pub type Multisigs<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Blake2_128Concat,
[u8; 32],
Multisig<BlockNumberFor<T>, BalanceOf<T>, T::AccountId, T::MaxSignatories>,
>;
#[pallet::error]
pub enum Error<T> {
MinimumThreshold,
AlreadyApproved,
NoApprovalsNeeded,
TooFewSignatories,
TooManySignatories,
SignatoriesOutOfOrder,
SenderInSignatories,
NotFound,
NotOwner,
NoTimepoint,
WrongTimepoint,
UnexpectedTimepoint,
MaxWeightTooLow,
AlreadyStored,
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
NewMultisig { approving: T::AccountId, multisig: T::AccountId, call_hash: CallHash },
MultisigApproval {
approving: T::AccountId,
timepoint: Timepoint<BlockNumberFor<T>>,
multisig: T::AccountId,
call_hash: CallHash,
},
MultisigExecuted {
approving: T::AccountId,
timepoint: Timepoint<BlockNumberFor<T>>,
multisig: T::AccountId,
call_hash: CallHash,
result: DispatchResult,
},
MultisigCancelled {
cancelling: T::AccountId,
timepoint: Timepoint<BlockNumberFor<T>>,
multisig: T::AccountId,
call_hash: CallHash,
},
DepositPoked {
who: T::AccountId,
call_hash: CallHash,
old_deposit: BalanceOf<T>,
new_deposit: BalanceOf<T>,
},
}
#[pallet::hooks]
impl<T: Config> Hooks<frame_system::pallet_prelude::BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight({
let dispatch_info = call.get_dispatch_info();
(
T::WeightInfo::as_multi_threshold_1(call.using_encoded(|c| c.len() as u32))
// AccountData for inner call origin accountdata.
.saturating_add(T::DbWeight::get().reads_writes(1, 1))
.saturating_add(dispatch_info.call_weight),
dispatch_info.class,
)
})]
pub fn as_multi_threshold_1(
origin: OriginFor<T>,
other_signatories: Vec<T::AccountId>,
call: Box<<T as Config>::RuntimeCall>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let max_sigs = T::MaxSignatories::get() as usize;
ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
let other_signatories_len = other_signatories.len();
ensure!(other_signatories_len < max_sigs, Error::<T>::TooManySignatories);
let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
let id = Self::multi_account_id(&signatories, 1);
let (call_len, call_hash) = call.using_encoded(|c| (c.len(), blake2_256(&c)));
let result = call.dispatch(RawOrigin::Signed(id.clone()).into());
Self::deposit_event(Event::MultisigExecuted {
approving: who,
timepoint: Self::timepoint(),
multisig: id,
call_hash,
result: result.map(|_| ()).map_err(|e| e.error),
});
result
.map(|post_dispatch_info| {
post_dispatch_info
.actual_weight
.map(|actual_weight| {
T::WeightInfo::as_multi_threshold_1(call_len as u32)
.saturating_add(actual_weight)
})
.into()
})
.map_err(|err| match err.post_info.actual_weight {
Some(actual_weight) => {
let weight_used = T::WeightInfo::as_multi_threshold_1(call_len as u32)
.saturating_add(actual_weight);
let post_info = Some(weight_used).into();
DispatchErrorWithPostInfo { post_info, error: err.error }
},
None => err,
})
}
#[pallet::call_index(1)]
#[pallet::weight({
let s = other_signatories.len() as u32;
let z = call.using_encoded(|d| d.len()) as u32;
T::WeightInfo::as_multi_create(s, z)
.max(T::WeightInfo::as_multi_approve(s, z))
.max(T::WeightInfo::as_multi_complete(s, z))
.saturating_add(*max_weight)
})]
pub fn as_multi(
origin: OriginFor<T>,
threshold: u16,
other_signatories: Vec<T::AccountId>,
maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
call: Box<<T as Config>::RuntimeCall>,
max_weight: Weight,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Self::operate(
who,
threshold,
other_signatories,
maybe_timepoint,
CallOrHash::Call(*call),
max_weight,
)
}
#[pallet::call_index(2)]
#[pallet::weight({
let s = other_signatories.len() as u32;
T::WeightInfo::approve_as_multi_create(s)
.max(T::WeightInfo::approve_as_multi_approve(s))
.saturating_add(*max_weight)
})]
pub fn approve_as_multi(
origin: OriginFor<T>,
threshold: u16,
other_signatories: Vec<T::AccountId>,
maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
call_hash: [u8; 32],
max_weight: Weight,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Self::operate(
who,
threshold,
other_signatories,
maybe_timepoint,
CallOrHash::Hash(call_hash),
max_weight,
)
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::cancel_as_multi(other_signatories.len() as u32))]
pub fn cancel_as_multi(
origin: OriginFor<T>,
threshold: u16,
other_signatories: Vec<T::AccountId>,
timepoint: Timepoint<BlockNumberFor<T>>,
call_hash: [u8; 32],
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
let max_sigs = T::MaxSignatories::get() as usize;
ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
ensure!(other_signatories.len() < max_sigs, Error::<T>::TooManySignatories);
let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
let id = Self::multi_account_id(&signatories, threshold);
let m = <Multisigs<T>>::get(&id, call_hash).ok_or(Error::<T>::NotFound)?;
ensure!(m.when == timepoint, Error::<T>::WrongTimepoint);
ensure!(m.depositor == who, Error::<T>::NotOwner);
let err_amount = T::Currency::unreserve(&m.depositor, m.deposit);
debug_assert!(err_amount.is_zero());
<Multisigs<T>>::remove(&id, &call_hash);
Self::deposit_event(Event::MultisigCancelled {
cancelling: who,
timepoint,
multisig: id,
call_hash,
});
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::poke_deposit(other_signatories.len() as u32))]
pub fn poke_deposit(
origin: OriginFor<T>,
threshold: u16,
other_signatories: Vec<T::AccountId>,
call_hash: [u8; 32],
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
let max_sigs = T::MaxSignatories::get() as usize;
ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
ensure!(other_signatories.len() < max_sigs, Error::<T>::TooManySignatories);
let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
let id = Self::multi_account_id(&signatories, threshold);
Multisigs::<T>::try_mutate(
&id,
call_hash,
|maybe_multisig| -> DispatchResultWithPostInfo {
let mut multisig = maybe_multisig.take().ok_or(Error::<T>::NotFound)?;
ensure!(multisig.depositor == who, Error::<T>::NotOwner);
let new_deposit = Self::deposit(threshold);
let old_deposit = multisig.deposit;
if new_deposit == old_deposit {
*maybe_multisig = Some(multisig);
return Ok(Pays::Yes.into());
}
if new_deposit > old_deposit {
let extra = new_deposit.saturating_sub(old_deposit);
T::Currency::reserve(&who, extra)?;
} else {
let excess = old_deposit.saturating_sub(new_deposit);
let remaining_unreserved = T::Currency::unreserve(&who, excess);
if !remaining_unreserved.is_zero() {
defensive!(
"Failed to unreserve for full amount for multisig. (Call Hash, Requested, Actual): ",
(call_hash, excess, excess.saturating_sub(remaining_unreserved))
);
}
}
multisig.deposit = new_deposit;
*maybe_multisig = Some(multisig);
Self::deposit_event(Event::DepositPoked {
who: who.clone(),
call_hash,
old_deposit,
new_deposit,
});
Ok(Pays::No.into())
},
)
}
}
}
impl<T: Config> Pallet<T> {
pub fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId {
let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(blake2_256);
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
.expect("infinite length input; no invalid inputs for type; qed")
}
fn operate(
who: T::AccountId,
threshold: u16,
other_signatories: Vec<T::AccountId>,
maybe_timepoint: Option<Timepoint<BlockNumberFor<T>>>,
call_or_hash: CallOrHash<T>,
max_weight: Weight,
) -> DispatchResultWithPostInfo {
ensure!(threshold >= 2, Error::<T>::MinimumThreshold);
let max_sigs = T::MaxSignatories::get() as usize;
ensure!(!other_signatories.is_empty(), Error::<T>::TooFewSignatories);
let other_signatories_len = other_signatories.len();
ensure!(other_signatories_len < max_sigs, Error::<T>::TooManySignatories);
let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?;
let id = Self::multi_account_id(&signatories, threshold);
let (call_hash, call_len, maybe_call) = match call_or_hash {
CallOrHash::Call(call) => {
let (call_hash, call_len) = call.using_encoded(|d| (blake2_256(d), d.len()));
(call_hash, call_len, Some(call))
},
CallOrHash::Hash(h) => (h, 0, None),
};
if let Some(mut m) = <Multisigs<T>>::get(&id, call_hash) {
let timepoint = maybe_timepoint.ok_or(Error::<T>::NoTimepoint)?;
ensure!(m.when == timepoint, Error::<T>::WrongTimepoint);
let mut approvals = m.approvals.len() as u16;
let maybe_pos = m.approvals.binary_search(&who).err().filter(|_| approvals < threshold);
if maybe_pos.is_some() {
approvals += 1;
}
if let Some(call) = maybe_call.filter(|_| approvals >= threshold) {
ensure!(
call.get_dispatch_info().call_weight.all_lte(max_weight),
Error::<T>::MaxWeightTooLow
);
<Multisigs<T>>::remove(&id, call_hash);
T::Currency::unreserve(&m.depositor, m.deposit);
let result = call.dispatch(RawOrigin::Signed(id.clone()).into());
Self::deposit_event(Event::MultisigExecuted {
approving: who,
timepoint,
multisig: id,
call_hash,
result: result.map(|_| ()).map_err(|e| e.error),
});
Ok(get_result_weight(result)
.map(|actual_weight| {
T::WeightInfo::as_multi_complete(
other_signatories_len as u32,
call_len as u32,
)
.saturating_add(actual_weight)
})
.into())
} else {
if let Some(pos) = maybe_pos {
m.approvals
.try_insert(pos, who.clone())
.map_err(|_| Error::<T>::TooManySignatories)?;
<Multisigs<T>>::insert(&id, call_hash, m);
Self::deposit_event(Event::MultisigApproval {
approving: who,
timepoint,
multisig: id,
call_hash,
});
} else {
Err(Error::<T>::AlreadyApproved)?
}
let final_weight =
T::WeightInfo::as_multi_approve(other_signatories_len as u32, call_len as u32);
Ok(Some(final_weight).into())
}
} else {
ensure!(maybe_timepoint.is_none(), Error::<T>::UnexpectedTimepoint);
let deposit = Self::deposit(threshold);
T::Currency::reserve(&who, deposit)?;
let initial_approvals =
vec![who.clone()].try_into().map_err(|_| Error::<T>::TooManySignatories)?;
<Multisigs<T>>::insert(
&id,
call_hash,
Multisig {
when: Self::timepoint(),
deposit,
depositor: who.clone(),
approvals: initial_approvals,
},
);
Self::deposit_event(Event::NewMultisig { approving: who, multisig: id, call_hash });
let final_weight =
T::WeightInfo::as_multi_create(other_signatories_len as u32, call_len as u32);
Ok(Some(final_weight).into())
}
}
pub fn timepoint() -> Timepoint<BlockNumberFor<T>> {
Timepoint {
height: T::BlockNumberProvider::current_block_number(),
index: <frame_system::Pallet<T>>::extrinsic_index().unwrap_or_default(),
}
}
fn ensure_sorted_and_insert(
other_signatories: Vec<T::AccountId>,
who: T::AccountId,
) -> Result<Vec<T::AccountId>, DispatchError> {
let mut signatories = other_signatories;
let mut maybe_last = None;
let mut index = 0;
for item in signatories.iter() {
if let Some(last) = maybe_last {
ensure!(last < item, Error::<T>::SignatoriesOutOfOrder);
}
if item <= &who {
ensure!(item != &who, Error::<T>::SenderInSignatories);
index += 1;
}
maybe_last = Some(item);
}
signatories.insert(index, who);
Ok(signatories)
}
pub fn deposit(threshold: u16) -> BalanceOf<T> {
T::DepositBase::get() + T::DepositFactor::get() * threshold.into()
}
}
fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
match result {
Ok(post_info) => post_info.actual_weight,
Err(err) => err.post_info.actual_weight,
}
}