diff --git a/new/0074-stateful-multisig-pallet.html b/new/0074-stateful-multisig-pallet.html index 4dad10b..1671086 100644 --- a/new/0074-stateful-multisig-pallet.html +++ b/new/0074-stateful-multisig-pallet.html @@ -213,12 +213,12 @@

RFC-0074: Stateful Multisig Pallet

- +
Start Date15 February 2024
DescriptionAdd Enhanced Multisig Pallet to Collectives chain
DescriptionAdd Enhanced Multisig Pallet to System chains
AuthorsAbdelrahman Soliman (Boda)

Summary

-

A pallet to facilitate enhanced multisig accounts. The main enhancement is that we store a multisig account in the state with related info (owners, threshold,..etc). The module affords enhanced control over administrative operations such as adding/removing owners, changing the threshold, account deletion, canceling an existing proposal. Each owner can approve/revoke a proposal while still exists. The proposal is not intended for migrating or getting rid of existing multisig. It's to allow both options to coexist.

+

A pallet to facilitate enhanced multisig accounts. The main enhancement is that we store a multisig account in the state with related info (signers, threshold,..etc). The module affords enhanced control over administrative operations such as adding/removing signers, changing the threshold, account deletion, canceling an existing proposal. Each signer can approve/reject a proposal while still exists. The proposal is not intended for migrating or getting rid of existing multisig. It's to allow both options to coexist.

For the rest of the RFC We use the following terms:

State Transition Functions

-

All functions have detailed rustdoc in PR#3300. Here is a brief overview of the functions:

+
#![allow(unused)]
+fn main() {
+		/// Creates a new multisig account and attach signers with a threshold to it.
+		///
+		/// The dispatch origin for this call must be _Signed_. It is expected to be a nomral AccountId and not a
+		/// Multisig AccountId.
+		///
+		/// T::BaseCreationDeposit + T::PerSignerDeposit * signers.len() will be held from the caller's account.
+		///
+		/// # Arguments
+		///
+		/// - `signers`: Initial set of accounts to add to the multisig. These may be updated later via `add_signer`
+		/// and `remove_signer`.
+		/// - `threshold`: The threshold number of accounts required to approve an action. Must be greater than 0 and
+		/// less than or equal to the total number of signers.
+		///
+		/// # Errors
+		///
+		/// * `TooManySignatories` - The number of signatories exceeds the maximum allowed.
+		/// * `InvalidThreshold` - The threshold is greater than the total number of signers.
+		pub fn create_multisig(
+			origin: OriginFor<T>,
+			signers: BoundedBTreeSet<T::AccountId, T::MaxSignatories>,
+			threshold: u32,
+		) -> DispatchResult 
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Starts a new proposal for a dispatchable call for a multisig account.
+		/// The caller must be one of the signers of the multisig account.
+		/// T::ProposalDeposit will be held from the caller's account.
+		///
+		/// # Arguments
+		///
+		/// * `multisig_account` - The multisig account ID.
+		/// * `call` - The dispatchable call to be executed.
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `UnAuthorizedSigner` - The caller is not an signer of the multisig account.
+		/// * `TooManySignatories` - The number of signatories exceeds the maximum allowed. (shouldn't really happen as it's the first approval)
+		pub fn start_proposal(
+			origin: OriginFor<T>,
+			multisig_account: T::AccountId,
+			call_hash: T::Hash,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Approves a proposal for a dispatchable call for a multisig account.
+		/// The caller must be one of the signers of the multisig account.
+		///
+		/// If a signer did approve -> reject -> approve, the proposal will be approved.
+		/// If a signer did approve -> reject, the proposal will be rejected.
+		///
+		/// # Arguments
+		///
+		/// * `multisig_account` - The multisig account ID.
+		/// * `call_hash` - The hash of the call to be approved. (This will be the hash of the call that was used in `start_proposal`)
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `UnAuthorizedSigner` - The caller is not an signer of the multisig account.
+		/// * `TooManySignatories` - The number of signatories exceeds the maximum allowed.
+		/// This shouldn't really happen as it's an approval, not an addition of a new signer.
+		pub fn approve(
+			origin: OriginFor<T>,
+			multisig_account: T::AccountId,
+			call_hash: T::Hash,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Rejects a proposal for a multisig account.
+		/// The caller must be one of the signers of the multisig account.
+		///
+		/// Between approving and rejecting, last call wins.
+		/// If a signer did approve -> reject -> approve, the proposal will be approved.
+		/// If a signer did approve -> reject, the proposal will be rejected.
+		///
+		/// # Arguments
+		///
+		/// * `multisig_account` - The multisig account ID.
+		/// * `call_hash` - The hash of the call to be approved. (This will be the hash of the call that was used in `start_proposal`)
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `UnAuthorizedSigner` - The caller is not an signer of the multisig account.
+		/// * `SignerNotFound` - The caller has not approved the proposal.
+		#[pallet::call_index(3)]
+		#[pallet::weight(Weight::default())]
+		pub fn reject(
+			origin: OriginFor<T>,
+			multisig_account: T::AccountId,
+			call_hash: T::Hash,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Executes a proposal for a dispatchable call for a multisig account.
+		/// Poropsal needs to be approved by enough signers (exceeds or equal multisig threshold) before it can be executed.
+		/// The caller must be one of the signers of the multisig account.
+		///
+		/// This function does an extra check to make sure that all approvers still exist in the multisig account.
+		/// That is to make sure that the multisig account is not compromised by removing an signer during an active proposal.
+		///
+		/// Once finished, the withheld deposit will be returned to the proposal creator.
+		///
+		/// # Arguments
+		///
+		/// * `multisig_account` - The multisig account ID.
+		/// * `call` - The call to be executed.
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `UnAuthorizedSigner` - The caller is not an signer of the multisig account.
+		/// * `NotEnoughApprovers` - approvers don't exceed the threshold.
+		pub fn execute_proposal(
+			origin: OriginFor<T>,
+			multisig_account: T::AccountId,
+			call: Box<<T as Config>::RuntimeCall>,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Cancels an existing proposal for a multisig account.
+		/// Poropsal needs to be rejected by enough signers (exceeds or equal multisig threshold) before it can be executed.
+		/// The caller must be one of the signers of the multisig account.
+		///
+		/// This function does an extra check to make sure that all rejectors still exist in the multisig account.
+		/// That is to make sure that the multisig account is not compromised by removing an signer during an active proposal.
+		///
+		/// Once finished, the withheld deposit will be returned to the proposal creator./
+		///
+		/// # Arguments
+		///
+		/// * `origin` - The origin multisig account who wants to cancel the proposal.
+		/// * `call_hash` - The hash of the call to be canceled. (This will be the hash of the call that was used in `start_proposal`)
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `ProposalNotFound` - The proposal does not exist.
+		pub fn cancel_proposal(
+		origin: OriginFor<T>, 
+		multisig_account: T::AccountId, 
+		call_hash: T::Hash) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Cancels an existing proposal for a multisig account Only if the proposal doesn't have approvers other than
+		/// the proposer.
+		///
+		///	This function needs to be called from a the proposer of the proposal as the origin.
+		///
+		/// The withheld deposit will be returned to the proposal creator.
+		///
+		/// # Arguments
+		///
+		/// * `multisig_account` - The multisig account ID.
+		/// * `call_hash` - The hash of the call to be canceled. (This will be the hash of the call that was used in `start_proposal`)
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `ProposalNotFound` - The proposal does not exist.
+		pub fn cancel_own_proposal(
+			origin: OriginFor<T>,
+			multisig_account: T::AccountId,
+			call_hash: T::Hash,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Cleanup proposals of a multisig account. This function will iterate over a max limit per extrinsic to ensure
+		/// we don't have unbounded iteration over the proposals.
+		///
+		/// The withheld deposit will be returned to the proposal creator.
+		///
+		/// # Arguments
+		///
+		/// * `multisig_account` - The multisig account ID.
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `ProposalNotFound` - The proposal does not exist.
+		pub fn cleanup_proposals(
+			origin: OriginFor<T>,
+			multisig_account: T::AccountId,
+		) -> DispatchResult
+}

Note: Next functions need to be called from the multisig account itself. Deposits are reserved from the multisig account as well.

+
#![allow(unused)]
+fn main() {
+		/// Adds a new signer to the multisig account.
+		/// This function needs to be called from a Multisig account as the origin.
+		/// Otherwise it will fail with MultisigNotFound error.
+		///
+		/// T::PerSignerDeposit will be held from the multisig account.
+		///
+		/// # Arguments
+		///
+		/// * `origin` - The origin multisig account who wants to add a new signer to the multisig account.
+		/// * `new_signer` - The AccountId of the new signer to be added.
+		/// * `new_threshold` - The new threshold for the multisig account after adding the new signer.
+		///
+		/// # Errors
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `InvalidThreshold` - The threshold is greater than the total number of signers or is zero.
+		/// * `TooManySignatories` - The number of signatories exceeds the maximum allowed.
+		pub fn add_signer(
+			origin: OriginFor<T>,
+			new_signer: T::AccountId,
+			new_threshold: u32,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Removes an  signer from the multisig account.
+		/// This function needs to be called from a Multisig account as the origin.
+		/// Otherwise it will fail with MultisigNotFound error.
+		/// If only one signer exists and is removed, the multisig account and any pending proposals for this account will be deleted from the state.
+		///
+		/// # Arguments
+		///
+		/// * `origin` - The origin multisig account who wants to remove an signer from the multisig account.
+		/// * `signer_to_remove` - The AccountId of the signer to be removed.
+		/// * `new_threshold` - The new threshold for the multisig account after removing the signer. Accepts zero if
+		/// the signer is the only one left.kkk
+		///
+		/// # Errors
+		///
+		/// This function can return the following errors:
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `InvalidThreshold` - The new threshold is greater than the total number of signers or is zero.
+		/// * `UnAuthorizedSigner` - The caller is not an signer of the multisig account.
+		pub fn remove_signer(
+			origin: OriginFor<T>,
+			signer_to_remove: T::AccountId,
+			new_threshold: u32,
+		) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Sets a new threshold for a multisig account.
+		///	This function needs to be called from a Multisig account as the origin.
+		/// Otherwise it will fail with MultisigNotFound error.
+		///
+		/// # Arguments
+		///
+		/// * `origin` - The origin multisig account who wants to set the new threshold.
+		/// * `new_threshold` - The new threshold to be set.
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		/// * `InvalidThreshold` - The new threshold is greater than the total number of signers or is zero.
+		set_threshold(origin: OriginFor<T>, new_threshold: u32) -> DispatchResult
+}
+ +
#![allow(unused)]
+fn main() {
+		/// Deletes a multisig account and all related proposals.
+		///
+		///	This function needs to be called from a Multisig account as the origin.
+		/// Otherwise it will fail with MultisigNotFound error.
+		///
+		/// # Arguments
+		///
+		/// * `origin` - The origin multisig account who wants to cancel the proposal.
+		///
+		/// # Errors
+		///
+		/// * `MultisigNotFound` - The multisig account does not exist.
+		pub fn delete_account(origin: OriginFor<T>) -> DispatchResult
+}

Storage/State