diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 488b29aa2aab9fac760d43ce1bd91f90b902beb0..3b7fcb19c3b05f072b20397b808acc4890f9aebb 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -128,9 +128,21 @@ impl frame_system::Trait for Runtime { type ModuleToIndex = (); } +parameter_types! { + // One storage item; value is size 4+4+16+32 bytes = 56 bytes. + pub const MultisigDepositBase: Balance = 30 * CENTS; + // Additional storage item size of 32 bytes. + pub const MultisigDepositFactor: Balance = 5 * CENTS; + pub const MaxSignatories: u16 = 100; +} + impl pallet_utility::Trait for Runtime { type Event = Event; type Call = Call; + type Currency = Balances; + type MultisigDepositBase = MultisigDepositBase; + type MultisigDepositFactor = MultisigDepositFactor; + type MaxSignatories = MaxSignatories; } parameter_types! { @@ -515,7 +527,7 @@ construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic { System: frame_system::{Module, Call, Storage, Config, Event}, - Utility: pallet_utility::{Module, Call, Event}, + Utility: pallet_utility::{Module, Call, Storage, Event<T>, Error}, Babe: pallet_babe::{Module, Call, Storage, Config, Inherent(Timestamp)}, Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, Authorship: pallet_authorship::{Module, Call, Storage, Inherent}, diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index 94ec8f73df9f6c183e3d8347f7d91be060b43a7e..8cf9e60d44bb39bd0c836d5d80a048c0ffc38dac 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -223,7 +223,6 @@ decl_error! { /// Balance should be non-zero BalanceZero, } - } decl_storage! { diff --git a/substrate/frame/metadata/src/lib.rs b/substrate/frame/metadata/src/lib.rs index d47e0c75cfdb80fb7bcb4a8cf3088dabd81bb9ee..9d829ab192345a3ac28d5c820e5d84b44a1872ea 100644 --- a/substrate/frame/metadata/src/lib.rs +++ b/substrate/frame/metadata/src/lib.rs @@ -273,6 +273,7 @@ impl sp_std::fmt::Debug for DefaultByteGetter { pub enum StorageHasher { Blake2_128, Blake2_256, + Blake2_128Concat, Twox128, Twox256, Twox64Concat, diff --git a/substrate/frame/support/procedural/src/lib.rs b/substrate/frame/support/procedural/src/lib.rs index d9c2fe03c9bf51e4621afef816d7d855c082f901..477663b681ed11c949e07185f60d90b2140fcb81 100644 --- a/substrate/frame/support/procedural/src/lib.rs +++ b/substrate/frame/support/procedural/src/lib.rs @@ -153,6 +153,7 @@ use proc_macro::TokenStream; /// * `twox_64_concat` - TwoX with 64bit + key concatenated. /// * `twox_128` - TwoX with 128bit. /// * `twox_256` - TwoX with with 256bit. +/// * `blake2_128_concat` - Blake2 with 128bit + key concatenated. /// * `blake2_128` - Blake2 with 128bit. /// * `blake2_256` - Blake2 with 256bit. /// diff --git a/substrate/frame/support/procedural/src/storage/mod.rs b/substrate/frame/support/procedural/src/storage/mod.rs index 8fbb97d916ba9e7435d89732c0947e5d2601530a..ef199c92c458e3456230403702639c0902327a81 100644 --- a/substrate/frame/support/procedural/src/storage/mod.rs +++ b/substrate/frame/support/procedural/src/storage/mod.rs @@ -368,6 +368,7 @@ pub struct ExtraGenesisLineDef { pub enum HasherKind { Blake2_256, Blake2_128, + Blake2_128Concat, Twox256, Twox128, Twox64Concat, @@ -378,6 +379,7 @@ impl HasherKind { match self { HasherKind::Blake2_256 => quote!( Blake2_256 ), HasherKind::Blake2_128 => quote!( Blake2_128 ), + HasherKind::Blake2_128Concat => quote!( Blake2_128Concat ), HasherKind::Twox256 => quote!( Twox256 ), HasherKind::Twox128 => quote!( Twox128 ), HasherKind::Twox64Concat => quote!( Twox64Concat ), @@ -388,6 +390,7 @@ impl HasherKind { match self { HasherKind::Blake2_256 => quote!( StorageHasher::Blake2_256 ), HasherKind::Blake2_128 => quote!( StorageHasher::Blake2_128 ), + HasherKind::Blake2_128Concat => quote!( StorageHasher::Blake2_128Concat ), HasherKind::Twox256 => quote!( StorageHasher::Twox256 ), HasherKind::Twox128 => quote!( StorageHasher::Twox128 ), HasherKind::Twox64Concat => quote!( StorageHasher::Twox64Concat ), diff --git a/substrate/frame/support/procedural/src/storage/parse.rs b/substrate/frame/support/procedural/src/storage/parse.rs index a5cd14aa1fd2d66085d2985fa42df9aedbe9a56d..2fe37da2569df9393e3fd04737e1d74dfc23fab7 100644 --- a/substrate/frame/support/procedural/src/storage/parse.rs +++ b/substrate/frame/support/procedural/src/storage/parse.rs @@ -31,6 +31,7 @@ mod keyword { syn::custom_keyword!(double_map); syn::custom_keyword!(blake2_256); syn::custom_keyword!(blake2_128); + syn::custom_keyword!(blake2_128_concat); syn::custom_keyword!(twox_256); syn::custom_keyword!(twox_128); syn::custom_keyword!(twox_64_concat); @@ -179,6 +180,7 @@ struct DeclStorageDoubleMap { enum Hasher { Blake2_256(keyword::blake2_256), Blake2_128(keyword::blake2_128), + Blake2_128Concat(keyword::blake2_128_concat), Twox256(keyword::twox_256), Twox128(keyword::twox_128), Twox64Concat(keyword::twox_64_concat), @@ -207,6 +209,7 @@ impl From<Hasher> for super::HasherKind { match hasher { Hasher::Blake2_256(_) => super::HasherKind::Blake2_256, Hasher::Blake2_128(_) => super::HasherKind::Blake2_128, + Hasher::Blake2_128Concat(_) => super::HasherKind::Blake2_128Concat, Hasher::Twox256(_) => super::HasherKind::Twox256, Hasher::Twox128(_) => super::HasherKind::Twox128, Hasher::Twox64Concat(_) => super::HasherKind::Twox64Concat, diff --git a/substrate/frame/support/src/hash.rs b/substrate/frame/support/src/hash.rs index d9c1247f7bcdb90f8169a50596dc012a12f7a5ab..332a8f4e42cd7a9ea5181b7d79bd1adee4914f61 100644 --- a/substrate/frame/support/src/hash.rs +++ b/substrate/frame/support/src/hash.rs @@ -24,6 +24,7 @@ use sp_io::hashing::{blake2_128, blake2_256, twox_64, twox_128, twox_256}; pub trait Hashable: Sized { fn blake2_128(&self) -> [u8; 16]; fn blake2_256(&self) -> [u8; 32]; + fn blake2_128_concat(&self) -> Vec<u8>; fn twox_128(&self) -> [u8; 16]; fn twox_256(&self) -> [u8; 32]; fn twox_64_concat(&self) -> Vec<u8>; @@ -36,6 +37,9 @@ impl<T: Codec> Hashable for T { fn blake2_256(&self) -> [u8; 32] { self.using_encoded(blake2_256) } + fn blake2_128_concat(&self) -> Vec<u8> { + self.using_encoded(Blake2_128Concat::hash) + } fn twox_128(&self) -> [u8; 16] { self.using_encoded(twox_128) } @@ -66,6 +70,19 @@ impl StorageHasher for Twox64Concat { } } +/// Hash storage keys with `concat(blake2_128(key), key)` +pub struct Blake2_128Concat; +impl StorageHasher for Blake2_128Concat { + type Output = Vec<u8>; + fn hash(x: &[u8]) -> Vec<u8> { + blake2_128(x) + .iter() + .chain(x.into_iter()) + .cloned() + .collect::<Vec<_>>() + } +} + /// Hash storage keys with blake2 128 pub struct Blake2_128; impl StorageHasher for Blake2_128 { @@ -111,4 +128,10 @@ mod tests { let r = Twox64Concat::hash(b"foo"); assert_eq!(r.split_at(8), (&twox_128(b"foo")[..8], &b"foo"[..])) } + + #[test] + fn test_blake2_128_concat() { + let r = Blake2_128Concat::hash(b"foo"); + assert_eq!(r.split_at(16), (&blake2_128(b"foo")[..], &b"foo"[..])) + } } diff --git a/substrate/frame/support/src/lib.rs b/substrate/frame/support/src/lib.rs index f0357cff2f40ece59f76f608669d252709a88089..56afc793330b5a1a634cd74bce21cbbe904ff7c2 100644 --- a/substrate/frame/support/src/lib.rs +++ b/substrate/frame/support/src/lib.rs @@ -66,7 +66,9 @@ pub mod error; pub mod traits; pub mod weights; -pub use self::hash::{Twox256, Twox128, Blake2_256, Blake2_128, Twox64Concat, Hashable}; +pub use self::hash::{ + Twox256, Twox128, Blake2_256, Blake2_128, Twox64Concat, Blake2_128Concat, Hashable +}; pub use self::storage::{ StorageValue, StorageMap, StorageLinkedMap, StorageDoubleMap, StoragePrefixedMap }; diff --git a/substrate/frame/utility/Cargo.toml b/substrate/frame/utility/Cargo.toml index f74e059e369719d6fe47f96a06e41b22e07c7b68..afd7ae3d374c7d3385dc05bb789aeb97a0ee63c3 100644 --- a/substrate/frame/utility/Cargo.toml +++ b/substrate/frame/utility/Cargo.toml @@ -9,6 +9,7 @@ serde = { version = "1.0.101", optional = true } codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } frame-support = { version = "2.0.0", default-features = false, path = "../support" } frame-system = { version = "2.0.0", default-features = false, path = "../system" } +sp-core = { version = "2.0.0", default-features = false, path = "../../primitives/core" } sp-runtime = { version = "2.0.0", default-features = false, path = "../../primitives/runtime" } sp-std = { version = "2.0.0", default-features = false, path = "../../primitives/std" } sp-io = { version = "2.0.0", default-features = false, path = "../../primitives/io" } diff --git a/substrate/frame/utility/src/lib.rs b/substrate/frame/utility/src/lib.rs index 7f8552c4022730c46b4e6a440371c6fbc499c064..edc75abe539468da6c4fb10b3921a0575a83ed02 100644 --- a/substrate/frame/utility/src/lib.rs +++ b/substrate/frame/utility/src/lib.rs @@ -15,65 +15,645 @@ // along with Substrate. If not, see <http://www.gnu.org/licenses/>. //! # Utility Module -//! A module full of useful helpers for practical chain management. +//! A module with helpers for dispatch management. +//! +//! - [`utility::Trait`](./trait.Trait.html) +//! - [`Call`](./enum.Call.html) +//! +//! ## Overview +//! +//! This module contains three basic pieces of functionality, two of which are stateless: +//! - Batch dispatch: A stateless operation, allowing any origin to execute multiple calls in a +//! single dispatch. This can be useful to amalgamate proposals, combining `set_code` with +//! corresponding `set_storage`s, for efficient multiple payouts with just a single signature +//! verify, or in combination with one of the other two dispatch functionality. +//! - Pseudonymal dispatch: A stateless operation, allowing a signed origin to execute a call from +//! an alternative signed origin. Each account has 2**16 possible "pseudonyms" (alternative +//! account IDs) and these can be stacked. This can be useful as a key management tool, where you +//! need multiple distinct accounts (e.g. as controllers for many staking accounts), but where +//! it's perfectly fine to have each of them controlled by the same underlying keypair. +//! - Multisig dispatch (stateful): A potentially stateful operation, allowing multiple signed +//! origins (accounts) to coordinate and dispatch a call from a well-known origin, derivable +//! deterministically from the set of account IDs and the threshold number of accounts from the +//! set that must approve it. In the case that the threshold is just one then this is a stateless +//! operation. This is useful for multisig wallets where cryptographic threshold signatures are +//! not available or desired. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! #### For batch dispatch +//! * `batch` - Dispatch multiple calls from the sender's origin. +//! +//! #### For pseudonymal dispatch +//! * `as_sub` - Dispatch a call from a secondary ("sub") signed origin. +//! +//! #### For multisig dispatch +//! * `as_multi` - Approve and if possible dispatch a call from a composite origin formed from a +//! number of signed origins. +//! * `approve_as_multi` - Approve a call from a composite origin. +//! * `cancel_as_multi` - Cancel a call from a composite origin. +//! +//! [`Call`]: ./enum.Call.html +//! [`Trait`]: ./trait.Trait.html // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] use sp_std::prelude::*; -use frame_support::{decl_module, decl_event, Parameter, weights::SimpleDispatchInfo}; -use frame_system::{self as system, ensure_root}; -use sp_runtime::{traits::Dispatchable, DispatchError}; +use codec::{Encode, Decode}; +use sp_core::TypeId; +use sp_io::hashing::blake2_256; +use frame_support::{decl_module, decl_event, decl_error, decl_storage, Parameter, ensure, RuntimeDebug}; +use frame_support::{traits::{Get, ReservableCurrency, Currency}, weights::{ + GetDispatchInfo, ClassifyDispatch, WeighData, Weight, DispatchClass, PaysFee +}}; +use frame_system::{self as system, ensure_signed}; +use sp_runtime::{DispatchError, DispatchResult, traits::Dispatchable}; + +type BalanceOf<T> = <<T as Trait>::Currency as Currency<<T as frame_system::Trait>::AccountId>>::Balance; /// Configuration trait. pub trait Trait: frame_system::Trait { /// The overarching event type. - type Event: From<Event> + Into<<Self as frame_system::Trait>::Event>; + type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>; /// The overarching call type. - type Call: Parameter + Dispatchable<Origin=Self::Origin>; + type Call: Parameter + Dispatchable<Origin=Self::Origin> + GetDispatchInfo; + + /// The currency mechanism. + type Currency: ReservableCurrency<Self::AccountId>; + + /// The base amount of currency needed to reserve for creating a multisig execution. + /// + /// This is held for an additional storage item whose value size is + /// `4 + sizeof((BlockNumber, Balance, AccountId))` bytes. + type MultisigDepositBase: Get<BalanceOf<Self>>; + + /// The amount of currency needed per unit threshold when creating a multisig execution. + /// + /// This is held for adding 32 bytes more into a pre-existing storage value. + type MultisigDepositFactor: Get<BalanceOf<Self>>; + + /// The maximum amount of signatories allowed in the multisig. + type MaxSignatories: Get<u16>; +} + +/// A global extrinsic index, formed as the extrinsic index within a block, together with that +/// block's height. This allows a transaction in which a multisig operation of a particular +/// composite was created to be uniquely identified. +#[derive(Copy, Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)] +pub struct Timepoint<BlockNumber> { + /// The hieght of the chain at the point in time. + height: BlockNumber, + /// The index of the extrinsic at the point in time. + index: u32, +} + +/// An open multisig operation. +#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)] +pub struct Multisig<BlockNumber, Balance, AccountId> { + /// The extrinsic when the multisig operation was opened. + when: Timepoint<BlockNumber>, + /// The amount held in reserve of the `depositor`, to be returned once the operation ends. + deposit: Balance, + /// The account who opened it (i.e. the first to approve it). + depositor: AccountId, + /// The approvals achieved so far, including the depositor. Always sorted. + approvals: Vec<AccountId>, +} + +decl_storage! { + trait Store for Module<T: Trait> as Utility { + /// The set of open multisig operations. + pub Multisigs: double_map hasher(twox_64_concat) T::AccountId, blake2_128_concat([u8; 32]) + => Option<Multisig<T::BlockNumber, BalanceOf<T>, T::AccountId>>; + } +} + +decl_error! { + pub enum Error for Module<T: Trait> { + /// Threshold is too low (zero). + ZeroThreshold, + /// Call is already approved by this signatory. + AlreadyApproved, + /// Call doesn't need any (more) approvals. + NoApprovalsNeeded, + /// There are too few signatories in the list. + TooFewSignatories, + /// There are too many signatories in the list. + TooManySignatories, + /// The signatories were provided out of order; they should be ordered. + SignatoriesOutOfOrder, + /// The sender was contained in the other signatories; it shouldn't be. + SenderInSignatories, + /// Multisig operation not found when attempting to cancel. + NotFound, + /// Only the account that originally created the multisig is able to cancel it. + NotOwner, + /// No timepoint was given, yet the multisig operation is already underway. + NoTimepoint, + /// A different timepoint was given to the multisig operation that is underway. + WrongTimepoint, + /// A timepoint was given, yet no multisig operation is underway. + UnexpectedTimepoint, + } } -decl_event!( +decl_event! { /// Events type. - pub enum Event { - BatchExecuted(Vec<Result<(), DispatchError>>), + pub enum Event<T> where + AccountId = <T as system::Trait>::AccountId, + BlockNumber = <T as system::Trait>::BlockNumber + { + /// Batch of dispatches did not complete fully. Index of first failing dispatch given, as + /// well as the error. + BatchInterrupted(u32, DispatchError), + /// Batch of dispatches completed fully with no error. + BatchCompleted, + /// A new multisig operation has begun. First param is the account that is approving, + /// second is the multisig account. + NewMultisig(AccountId, AccountId), + /// A multisig operation has been approved by someone. First param is the account that is + /// approving, third is the multisig account. + MultisigApproval(AccountId, Timepoint<BlockNumber>, AccountId), + /// A multisig operation has been executed. First param is the account that is + /// approving, third is the multisig account. + MultisigExecuted(AccountId, Timepoint<BlockNumber>, AccountId, DispatchResult), + /// A multisig operation has been cancelled. First param is the account that is + /// cancelling, third is the multisig account. + MultisigCancelled(AccountId, Timepoint<BlockNumber>, AccountId), + } +} + +/// Simple index-based pass through for the weight functions. +struct Passthrough<Call>(sp_std::marker::PhantomData<Call>); + +impl<Call> Passthrough<Call> { + fn new() -> Self { Self(Default::default()) } +} +impl<Call: GetDispatchInfo> WeighData<(&u16, &Box<Call>)> for Passthrough<Call> { + fn weigh_data(&self, (_, call): (&u16, &Box<Call>)) -> Weight { + call.get_dispatch_info().weight + 10_000 + } +} +impl<Call: GetDispatchInfo> ClassifyDispatch<(&u16, &Box<Call>)> for Passthrough<Call> { + fn classify_dispatch(&self, (_, call): (&u16, &Box<Call>)) -> DispatchClass { + call.get_dispatch_info().class + } +} +impl<Call: GetDispatchInfo> PaysFee for Passthrough<Call> { + fn pays_fee(&self) -> bool { + true + } +} + +/// Sumation pass-through for the weight function of the batch call. +/// +/// This just adds all of the weights together of all of the calls. +struct BatchPassthrough<Call>(sp_std::marker::PhantomData<Call>); + +impl<Call> BatchPassthrough<Call> { + fn new() -> Self { Self(Default::default()) } +} +impl<Call: GetDispatchInfo> WeighData<(&Vec<Call>,)> for BatchPassthrough<Call> { + fn weigh_data(&self, (calls,): (&Vec<Call>,)) -> Weight { + calls.iter() + .map(|call| call.get_dispatch_info().weight) + .fold(10_000, |a, n| a + n) } +} +impl<Call: GetDispatchInfo> ClassifyDispatch<(&Vec<Call>,)> for BatchPassthrough<Call> { + fn classify_dispatch(&self, (_,): (&Vec<Call>,)) -> DispatchClass { + DispatchClass::Normal + } +} +impl<Call: GetDispatchInfo> PaysFee for BatchPassthrough<Call> { + fn pays_fee(&self) -> bool { + true + } +} + +/// Simple index-based pass through for the weight functions. +struct MultiPassthrough<Call, AccountId, Timepoint>( + sp_std::marker::PhantomData<(Call, AccountId, Timepoint)> ); +impl<Call, AccountId, Timepoint> MultiPassthrough<Call, AccountId, Timepoint> { + fn new() -> Self { Self(Default::default()) } +} +impl<Call: GetDispatchInfo, AccountId, Timepoint> WeighData<(&u16, &Vec<AccountId>, &Timepoint, &Box<Call>)> +for MultiPassthrough<Call, AccountId, Timepoint> +{ + fn weigh_data(&self, (_, sigs, _, call): (&u16, &Vec<AccountId>, &Timepoint, &Box<Call>)) -> Weight { + call.get_dispatch_info().weight + 10_000 * (sigs.len() as u32 + 1) + } +} +impl<Call: GetDispatchInfo, AccountId, Timepoint> ClassifyDispatch<(&u16, &Vec<AccountId>, &Timepoint, &Box<Call>)> +for MultiPassthrough<Call, AccountId, Timepoint> +{ + fn classify_dispatch(&self, (_, _, _, _): (&u16, &Vec<AccountId>, &Timepoint, &Box<Call>)) + -> DispatchClass + { + DispatchClass::Normal + } +} +impl<Call: GetDispatchInfo, AccountId, Timepoint> PaysFee +for MultiPassthrough<Call, AccountId, Timepoint> +{ + fn pays_fee(&self) -> bool { + true + } +} + +/// Simple index-based pass through for the weight functions. +struct SigsLen<AccountId, Timepoint>( + sp_std::marker::PhantomData<(AccountId, Timepoint)> +); + +impl<AccountId, Timepoint> SigsLen<AccountId, Timepoint> { + fn new() -> Self { Self(Default::default()) } +} +impl<AccountId, Timepoint> WeighData<(&u16, &Vec<AccountId>, &Timepoint, &[u8; 32])> +for SigsLen<AccountId, Timepoint> +{ + fn weigh_data(&self, (_, sigs, _, _): (&u16, &Vec<AccountId>, &Timepoint, &[u8; 32])) -> Weight { + 10_000 * (sigs.len() as u32 + 1) + } +} +impl<AccountId, Timepoint> ClassifyDispatch<(&u16, &Vec<AccountId>, &Timepoint, &[u8; 32])> +for SigsLen<AccountId, Timepoint> +{ + fn classify_dispatch(&self, (_, _, _, _): (&u16, &Vec<AccountId>, &Timepoint, &[u8; 32])) + -> DispatchClass + { + DispatchClass::Normal + } +} +impl<AccountId, Timepoint> PaysFee +for SigsLen<AccountId, Timepoint> +{ + fn pays_fee(&self) -> bool { + true + } +} + +/// A module identifier. These are per module and should be stored in a registry somewhere. +#[derive(Clone, Copy, Eq, PartialEq, Encode, Decode)] +struct IndexedUtilityModuleId(u16); + +impl TypeId for IndexedUtilityModuleId { + const TYPE_ID: [u8; 4] = *b"suba"; +} + decl_module! { pub struct Module<T: Trait> for enum Call where origin: T::Origin { /// Deposit one of this module's events by using the default implementation. fn deposit_event() = default; - /// Send a batch of dispatch calls (only root). - #[weight = SimpleDispatchInfo::FreeOperational] + /// Send a batch of dispatch calls. + /// + /// This will execute until the first one fails and then stop. + /// + /// May be called from any origin. + /// + /// - `calls`: The calls to be dispatched from the same origin. + /// + /// # <weight> + /// - The sum of the weights of the `calls`. + /// - One event. + /// # </weight> + /// + /// This will return `Ok` in all circumstances. To determine the success of the batch, an + /// event is deposited. If a call failed and the batch was interrupted, then the + /// `BatchInterrupted` event is deposited, along with the number of successful calls made + /// and the error of the failed call. If all were successful, then the `BatchCompleted` + /// event is deposited. + #[weight = <BatchPassthrough<<T as Trait>::Call>>::new()] fn batch(origin, calls: Vec<<T as Trait>::Call>) { - ensure_root(origin)?; - let results = calls.into_iter() - .map(|call| call.dispatch(frame_system::RawOrigin::Root.into())) - .map(|res| res.map_err(Into::into)) - .collect::<Vec<_>>(); - Self::deposit_event(Event::BatchExecuted(results)); + for (index, call) in calls.into_iter().enumerate() { + let result = call.dispatch(origin.clone()); + if let Err(e) = result { + Self::deposit_event(Event::<T>::BatchInterrupted(index as u32, e)); + return Ok(()); + } + } + Self::deposit_event(Event::<T>::BatchCompleted); + } + + /// Send a call through an indexed pseudonym of the sender. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// # <weight> + /// - The weight of the `call`. + /// # </weight> + #[weight = <Passthrough<<T as Trait>::Call>>::new()] + fn as_sub(origin, index: u16, call: Box<<T as Trait>::Call>) -> DispatchResult { + let who = ensure_signed(origin)?; + let pseudonym = Self::sub_account_id(who, index); + call.dispatch(frame_system::RawOrigin::Signed(pseudonym).into()) + } + + /// Register approval for a dispatch to be made from a deterministic composite account if + /// approved by a total of `threshold - 1` of `other_signatories`. + /// + /// If there are enough, then dispatch the call. + /// + /// Payment: `MultisigDepositBase` will be reserved if this is the first approval, plus + /// `threshold` times `MultisigDepositFactor`. It is returned once this dispatch happens or + /// is cancelled. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `threshold`: The total number of approvals for this dispatch before it is executed. + /// - `other_signatories`: The accounts (other than the sender) who can approve this + /// dispatch. May not be empty. + /// - `maybe_timepoint`: If this is the first approval, then this must be `None`. If it is + /// not the first approval, then it must be `Some`, with the timepoint (block number and + /// transaction index) of the first approval transaction. + /// - `call`: The call to be executed. + /// + /// NOTE: Unless this is the final approval, you will generally want to use + /// `approve_as_multi` instead, since it only requires a hash of the call. + /// + /// Result is equivalent to the dispatched result if `threshold` is exactly `1`. Otherwise + /// on success, result is `Ok` and the result from the interior call, if it was executed, + /// may be found in the deposited `MultisigExecuted` event. + /// + /// # <weight> + /// - `O(S + Z + Call)`. + /// - Up to one balance-reserve or unreserve operation. + /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of + /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. + /// - One call encode & hash, both of complexity `O(Z)` where `Z` is tx-len. + /// - One encode & hash, both of complexity `O(S)`. + /// - Up to one binary search and insert (`O(logS + S)`). + /// - I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. + /// - One event. + /// - The weight of the `call`. + /// - Storage: inserts one item, value size bounded by `MaxSignatories`, with a + /// deposit taken for its lifetime of + /// `MultisigDepositBase + threshold * MultisigDepositFactor`. + /// # </weight> + #[weight = <MultiPassthrough<<T as Trait>::Call, T::AccountId, Option<Timepoint<T::BlockNumber>>>>::new()] + fn as_multi(origin, + threshold: u16, + other_signatories: Vec<T::AccountId>, + maybe_timepoint: Option<Timepoint<T::BlockNumber>>, + call: Box<<T as Trait>::Call>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(threshold >= 1, Error::<T>::ZeroThreshold); + 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 call_hash = call.using_encoded(blake2_256); + + 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); + if let Err(pos) = m.approvals.binary_search(&who) { + // we know threshold is greater than zero from the above ensure. + if (m.approvals.len() as u16) < threshold - 1 { + m.approvals.insert(pos, who.clone()); + <Multisigs<T>>::insert(&id, call_hash, m); + Self::deposit_event(RawEvent::MultisigApproval(who, timepoint, id)); + return Ok(()) + } + } else { + if (m.approvals.len() as u16) < threshold { + Err(Error::<T>::AlreadyApproved)? + } + } + + let result = call.dispatch(frame_system::RawOrigin::Signed(id.clone()).into()); + let _ = T::Currency::unreserve(&m.depositor, m.deposit); + <Multisigs<T>>::remove(&id, call_hash); + Self::deposit_event(RawEvent::MultisigExecuted(who, timepoint, id, result)); + } else { + ensure!(maybe_timepoint.is_none(), Error::<T>::UnexpectedTimepoint); + if threshold > 1 { + let deposit = T::MultisigDepositBase::get() + + T::MultisigDepositFactor::get() * threshold.into(); + T::Currency::reserve(&who, deposit)?; + <Multisigs<T>>::insert(&id, call_hash, Multisig { + when: Self::timepoint(), + deposit, + depositor: who.clone(), + approvals: vec![who.clone()], + }); + Self::deposit_event(RawEvent::NewMultisig(who, id)); + } else { + return call.dispatch(frame_system::RawOrigin::Signed(id).into()) + } + } + Ok(()) + } + + /// Register approval for a dispatch to be made from a deterministic composite account if + /// approved by a total of `threshold - 1` of `other_signatories`. + /// + /// Payment: `MultisigDepositBase` will be reserved if this is the first approval, plus + /// `threshold` times `MultisigDepositFactor`. It is returned once this dispatch happens or + /// is cancelled. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `threshold`: The total number of approvals for this dispatch before it is executed. + /// - `other_signatories`: The accounts (other than the sender) who can approve this + /// dispatch. May not be empty. + /// - `maybe_timepoint`: If this is the first approval, then this must be `None`. If it is + /// not the first approval, then it must be `Some`, with the timepoint (block number and + /// transaction index) of the first approval transaction. + /// - `call_hash`: The hash of the call to be executed. + /// + /// NOTE: If this is the final approval, you will want to use `as_multi` instead. + /// + /// # <weight> + /// - `O(S)`. + /// - Up to one balance-reserve or unreserve operation. + /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of + /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. + /// - One encode & hash, both of complexity `O(S)`. + /// - Up to one binary search and insert (`O(logS + S)`). + /// - I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. + /// - One event. + /// - Storage: inserts one item, value size bounded by `MaxSignatories`, with a + /// deposit taken for its lifetime of + /// `MultisigDepositBase + threshold * MultisigDepositFactor`. + /// # </weight> + #[weight = <SigsLen<T::AccountId, Option<Timepoint<T::BlockNumber>>>>::new()] + fn approve_as_multi(origin, + threshold: u16, + other_signatories: Vec<T::AccountId>, + maybe_timepoint: Option<Timepoint<T::BlockNumber>>, + call_hash: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(threshold >= 1, Error::<T>::ZeroThreshold); + 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); + + 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); + ensure!(m.approvals.len() < threshold as usize, Error::<T>::NoApprovalsNeeded); + if let Err(pos) = m.approvals.binary_search(&who) { + m.approvals.insert(pos, who.clone()); + <Multisigs<T>>::insert(&id, call_hash, m); + Self::deposit_event(RawEvent::MultisigApproval(who, timepoint, id)); + } else { + Err(Error::<T>::AlreadyApproved)? + } + } else { + if threshold > 1 { + ensure!(maybe_timepoint.is_none(), Error::<T>::UnexpectedTimepoint); + let deposit = T::MultisigDepositBase::get() + + T::MultisigDepositFactor::get() * threshold.into(); + T::Currency::reserve(&who, deposit)?; + <Multisigs<T>>::insert(&id, call_hash, Multisig { + when: Self::timepoint(), + deposit, + depositor: who.clone(), + approvals: vec![who.clone()], + }); + Self::deposit_event(RawEvent::NewMultisig(who, id)); + } else { + Err(Error::<T>::NoApprovalsNeeded)? + } + } + Ok(()) + } + + /// Cancel a pre-existing, on-going multisig transaction. Any deposit reserved previously + /// for this operation will be unreserved on success. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `threshold`: The total number of approvals for this dispatch before it is executed. + /// - `other_signatories`: The accounts (other than the sender) who can approve this + /// dispatch. May not be empty. + /// - `timepoint`: The timepoint (block number and transaction index) of the first approval + /// transaction for this dispatch. + /// - `call_hash`: The hash of the call to be executed. + /// + /// # <weight> + /// - `O(S)`. + /// - Up to one balance-reserve or unreserve operation. + /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of + /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. + /// - One encode & hash, both of complexity `O(S)`. + /// - One event. + /// - I/O: 1 read `O(S)`, one remove. + /// - Storage: removes one item. + /// # </weight> + #[weight = <SigsLen<T::AccountId, Timepoint<T::BlockNumber>>>::new()] + fn cancel_as_multi(origin, + threshold: u16, + other_signatories: Vec<T::AccountId>, + timepoint: Timepoint<T::BlockNumber>, + call_hash: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(threshold >= 1, Error::<T>::ZeroThreshold); + 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 _ = T::Currency::unreserve(&m.depositor, m.deposit); + <Multisigs<T>>::remove(&id, call_hash); + + Self::deposit_event(RawEvent::MultisigCancelled(who, timepoint, id)); + Ok(()) } } } +impl<T: Trait> Module<T> { + /// Derive a sub-account ID from the owner account and the sub-account index. + pub fn sub_account_id(who: T::AccountId, index: u16) -> T::AccountId { + let entropy = (b"modlpy/utilisuba", who, index).using_encoded(blake2_256); + T::AccountId::decode(&mut &entropy[..]).unwrap_or_default() + } + + /// Derive a multi-account ID from the sorted list of accounts and the threshold that are + /// required. + /// + /// NOTE: `who` must be sorted. If it is not, then you'll get the wrong answer. + pub fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId { + let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(blake2_256); + T::AccountId::decode(&mut &entropy[..]).unwrap_or_default() + } + + /// The current `Timepoint`. + pub fn timepoint() -> Timepoint<T::BlockNumber> { + Timepoint { + height: <system::Module<T>>::block_number(), + index: <system::Module<T>>::extrinsic_count(), + } + } + + /// Check that signatories is sorted and doesn't contain sender, then insert sender. + 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) + } +} + #[cfg(test)] mod tests { use super::*; use frame_support::{ assert_ok, assert_noop, impl_outer_origin, parameter_types, impl_outer_dispatch, - weights::Weight + weights::Weight, impl_outer_event }; use sp_core::H256; - use sp_runtime::{Perbill, traits::{BlakeTwo256, IdentityLookup, BadOrigin}, testing::Header}; + use sp_runtime::{Perbill, traits::{BlakeTwo256, IdentityLookup}, testing::Header}; + use crate as utility; impl_outer_origin! { - pub enum Origin for Test where system = frame_system {} + pub enum Origin for Test where system = frame_system {} } + impl_outer_event! { + pub enum TestEvent for Test { + pallet_balances<T>, + utility<T>, + } + } impl_outer_dispatch! { pub enum Call for Test where origin: Origin { pallet_balances::Balances, @@ -102,7 +682,7 @@ mod tests { type AccountId = u64; type Lookup = IdentityLookup<Self::AccountId>; type Header = Header; - type Event = (); + type Event = TestEvent; type BlockHashCount = BlockHashCount; type MaximumBlockWeight = MaximumBlockWeight; type MaximumBlockLength = MaximumBlockLength; @@ -119,47 +699,360 @@ mod tests { type Balance = u64; type OnFreeBalanceZero = (); type OnNewAccount = (); - type Event = (); + type Event = TestEvent; type TransferPayment = (); type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type TransferFee = TransferFee; type CreationFee = CreationFee; } + parameter_types! { + pub const MultisigDepositBase: u64 = 1; + pub const MultisigDepositFactor: u64 = 1; + pub const MaxSignatories: u16 = 3; + } impl Trait for Test { - type Event = (); + type Event = TestEvent; type Call = Call; + type Currency = Balances; + type MultisigDepositBase = MultisigDepositBase; + type MultisigDepositFactor = MultisigDepositFactor; + type MaxSignatories = MaxSignatories; } type Balances = pallet_balances::Module<Test>; type Utility = Module<Test>; + use pallet_balances::Call as BalancesCall; + use pallet_balances::Error as BalancesError; + fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap(); pallet_balances::GenesisConfig::<Test> { - balances: vec![(1, 10), (2, 0)], + balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 10)], vesting: vec![], }.assimilate_storage(&mut t).unwrap(); t.into() } + fn last_event() -> TestEvent { + system::Module::<Test>::events().pop().map(|e| e.event).expect("Event expected") + } + + fn expect_event<E: Into<TestEvent>>(e: E) { + assert_eq!(last_event(), e.into()); + } + + fn now() -> Timepoint<u64> { + Utility::timepoint() + } + + #[test] + fn multisig_deposit_is_taken_and_returned() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_eq!(Balances::free_balance(1), 2); + assert_eq!(Balances::reserved_balance(1), 3); + + assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::reserved_balance(1), 0); + }); + } + #[test] - fn batch_works() { + fn cancel_multisig_returns_deposit() { new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Utility::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); + assert_ok!(Utility::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); + assert_eq!(Balances::free_balance(1), 6); + assert_eq!(Balances::reserved_balance(1), 4); + assert_ok!( + Utility::cancel_as_multi(Origin::signed(1), 3, vec![2, 3], now(), hash.clone()), + ); assert_eq!(Balances::free_balance(1), 10); - assert_eq!(Balances::free_balance(2), 0); + assert_eq!(Balances::reserved_balance(1), 0); + }); + } + + #[test] + fn timepoint_checking_works() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_noop!( - Utility::batch(Origin::signed(1), vec![ - Call::Balances(pallet_balances::Call::force_transfer(1, 2, 5)), - Call::Balances(pallet_balances::Call::force_transfer(1, 2, 5)) - ]), - BadOrigin, + Utility::approve_as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), hash.clone()), + Error::<Test>::UnexpectedTimepoint, ); + + assert_ok!(Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash)); + + assert_noop!( + Utility::as_multi(Origin::signed(2), 2, vec![1, 3], None, call.clone()), + Error::<Test>::NoTimepoint, + ); + let later = Timepoint { index: 1, .. now() }; + assert_noop!( + Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(later), call.clone()), + Error::<Test>::WrongTimepoint, + ); + }); + } + + #[test] + fn multisig_2_of_3_works() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash)); + assert_eq!(Balances::free_balance(6), 0); + + assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); + assert_eq!(Balances::free_balance(6), 15); + }); + } + + #[test] + fn multisig_3_of_3_works() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 3); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Utility::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); + assert_ok!(Utility::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); + assert_eq!(Balances::free_balance(6), 0); + + assert_ok!(Utility::as_multi(Origin::signed(3), 3, vec![1, 2], Some(now()), call)); + assert_eq!(Balances::free_balance(6), 15); + }); + } + + #[test] + fn cancel_multisig_works() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Utility::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); + assert_ok!(Utility::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); + assert_noop!( + Utility::cancel_as_multi(Origin::signed(2), 3, vec![1, 3], now(), hash.clone()), + Error::<Test>::NotOwner, + ); + assert_ok!( + Utility::cancel_as_multi(Origin::signed(1), 3, vec![2, 3], now(), hash.clone()), + ); + }); + } + + #[test] + fn multisig_2_of_3_as_multi_works() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_eq!(Balances::free_balance(6), 0); + + assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); + assert_eq!(Balances::free_balance(6), 15); + }); + } + + #[test] + fn multisig_2_of_3_as_multi_with_many_calls_works() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call1 = Box::new(Call::Balances(BalancesCall::transfer(6, 10))); + let call2 = Box::new(Call::Balances(BalancesCall::transfer(7, 5))); + + assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call1.clone())); + assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], None, call2.clone())); + assert_ok!(Utility::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call2)); + assert_ok!(Utility::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call1)); + + assert_eq!(Balances::free_balance(6), 10); + assert_eq!(Balances::free_balance(7), 5); + }); + } + + #[test] + fn multisig_2_of_3_cannot_reissue_same_call() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 10))); + assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call.clone())); + assert_eq!(Balances::free_balance(multi), 5); + + assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_ok!(Utility::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call)); + + let err = DispatchError::from(BalancesError::<Test, _>::InsufficientBalance).stripped(); + expect_event(RawEvent::MultisigExecuted(3, now(), multi, Err(err))); + }); + } + + #[test] + fn zero_threshold_fails() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_noop!( + Utility::as_multi(Origin::signed(1), 0, vec![2], None, call), + Error::<Test>::ZeroThreshold, + ); + }); + } + + #[test] + fn too_many_signatories_fails() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_noop!( + Utility::as_multi(Origin::signed(1), 2, vec![2, 3, 4], None, call.clone()), + Error::<Test>::TooManySignatories, + ); + }); + } + + #[test] + fn duplicate_approvals_are_ignored() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash.clone())); + assert_noop!( + Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], Some(now()), hash.clone()), + Error::<Test>::AlreadyApproved, + ); + assert_ok!(Utility::approve_as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), hash.clone())); + assert_noop!( + Utility::approve_as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), hash.clone()), + Error::<Test>::NoApprovalsNeeded, + ); + }); + } + + #[test] + fn multisig_1_of_3_works() { + new_test_ext().execute_with(|| { + let multi = Utility::multi_account_id(&[1, 2, 3][..], 1); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_noop!( + Utility::approve_as_multi(Origin::signed(1), 1, vec![2, 3], None, hash.clone()), + Error::<Test>::NoApprovalsNeeded, + ); + assert_noop!( + Utility::as_multi(Origin::signed(4), 1, vec![2, 3], None, call.clone()), + BalancesError::<Test, _>::InsufficientBalance, + ); + assert_ok!(Utility::as_multi(Origin::signed(1), 1, vec![2, 3], None, call)); + + assert_eq!(Balances::free_balance(6), 15); + }); + } + + #[test] + fn as_sub_works() { + new_test_ext().execute_with(|| { + let sub_1_0 = Utility::sub_account_id(1, 0); + assert_ok!(Balances::transfer(Origin::signed(1), sub_1_0, 5)); + assert_noop!(Utility::as_sub( + Origin::signed(1), + 1, + Box::new(Call::Balances(BalancesCall::transfer(6, 3))), + ), BalancesError::<Test, _>::InsufficientBalance); + assert_ok!(Utility::as_sub( + Origin::signed(1), + 0, + Box::new(Call::Balances(BalancesCall::transfer(2, 3))), + )); + assert_eq!(Balances::free_balance(sub_1_0), 2); + assert_eq!(Balances::free_balance(2), 13); + }); + } + + #[test] + fn batch_with_root_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); assert_ok!(Utility::batch(Origin::ROOT, vec![ - Call::Balances(pallet_balances::Call::force_transfer(1, 2, 5)), - Call::Balances(pallet_balances::Call::force_transfer(1, 2, 5)) + Call::Balances(BalancesCall::force_transfer(1, 2, 5)), + Call::Balances(BalancesCall::force_transfer(1, 2, 5)) ])); assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 20); + }); + } + + #[test] + fn batch_with_signed_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 10); + assert_ok!( + Utility::batch(Origin::signed(1), vec![ + Call::Balances(BalancesCall::transfer(2, 5)), + Call::Balances(BalancesCall::transfer(2, 5)) + ]), + ); + assert_eq!(Balances::free_balance(1), 0); + assert_eq!(Balances::free_balance(2), 20); + }); + } + + #[test] + fn batch_early_exit_works() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(1), 10); assert_eq!(Balances::free_balance(2), 10); + assert_ok!( + Utility::batch(Origin::signed(1), vec![ + Call::Balances(BalancesCall::transfer(2, 5)), + Call::Balances(BalancesCall::transfer(2, 10)), + Call::Balances(BalancesCall::transfer(2, 5)), + ]), + ); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::free_balance(2), 15); }); } } diff --git a/substrate/primitives/runtime/src/lib.rs b/substrate/primitives/runtime/src/lib.rs index 986b4bf660e77f5a9c81621c0623bf28644fa9ab..1e8178f05e168e3d97a2b994230cb45f297cc6e4 100644 --- a/substrate/primitives/runtime/src/lib.rs +++ b/substrate/primitives/runtime/src/lib.rs @@ -384,6 +384,17 @@ pub enum DispatchError { }, } +impl DispatchError { + /// Return the same error but without the attached message. + pub fn stripped(self) -> Self { + match self { + DispatchError::Module { index, error, message: Some(_) } + => DispatchError::Module { index, error, message: None }, + m => m, + } + } +} + impl From<crate::traits::LookupError> for DispatchError { fn from(_: crate::traits::LookupError) -> Self { Self::CannotLookup