Files
pezkuwi-fellows/stale/0074-stateful-multisig-pallet.html
T
2025-01-07 01:06:19 +00:00

830 lines
55 KiB
HTML

<!DOCTYPE HTML>
<html lang="en" class="polkadot" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>RFC-0074: Stateful Multisig Pallet - Polkadot Fellowship RFCs</title>
<!-- Custom HTML head -->
<meta name="description" content="An online book of RFCs approved or proposed within the Polkadot Fellowship.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<link rel="stylesheet" href="../theme/polkadot.css">
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "polkadot" : "polkadot";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('polkadot')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../introduction.html">Introduction</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Newly Proposed</li><li class="chapter-item expanded "><a href="../new/0135-compressed-blob-prefixes.html">RFC-0135: Compressed Blob Prefixes</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Proposed</li><li class="chapter-item expanded "><a href="../proposed/0004-remove-unnecessary-allocator-usage.html">RFC-0004: Remove the host-side runtime memory allocator</a></li><li class="chapter-item expanded "><a href="../proposed/0123-pending-code-as-storage-location-for-runtime-upgrades.html">RFC-0123: Introduce :pending_code as intermediate storage key for the runtime code</a></li><li class="chapter-item expanded "><a href="../proposed/0125-xcm-asset-metadata.html">RFC-0125: XCM Asset Metadata</a></li><li class="chapter-item expanded "><a href="../proposed/0126-introduce-xcq.html">RFC-0126: Introduce XCQ(Cross Consensus Query)</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Approved</li><li class="chapter-item expanded "><a href="../approved/0001-agile-coretime.html">RFC-1: Agile Coretime</a></li><li class="chapter-item expanded "><a href="../approved/0005-coretime-interface.html">RFC-5: Coretime Interface</a></li><li class="chapter-item expanded "><a href="../approved/0007-system-collator-selection.html">RFC-0007: System Collator Selection</a></li><li class="chapter-item expanded "><a href="../approved/0008-parachain-bootnodes-dht.html">RFC-0008: Store parachain bootnodes in relay chain DHT</a></li><li class="chapter-item expanded "><a href="../approved/0009-improved-net-light-client-requests.html">RFC-0009: Improved light client requests networking protocol</a></li><li class="chapter-item expanded "><a href="../approved/0010-burn-coretime-revenue.html">RFC-0010: Burn Coretime Revenue</a></li><li class="chapter-item expanded "><a href="../approved/0012-process-for-adding-new-collectives.html">RFC-0012: Process for Adding New System Collectives</a></li><li class="chapter-item expanded "><a href="../approved/0013-prepare-blockbuilder-and-core-runtime-apis-for-mbms.html">RFC-0013: Prepare Core runtime API for MBMs</a></li><li class="chapter-item expanded "><a href="../approved/0014-improve-locking-mechanism-for-parachains.html">RFC-0014: Improve locking mechanism for parachains</a></li><li class="chapter-item expanded "><a href="../approved/0022-adopt-encointer-runtime.html">RFC-0022: Adopt Encointer Runtime</a></li><li class="chapter-item expanded "><a href="../approved/0026-sassafras-consensus.html">RFC-0026: Sassafras Consensus Protocol</a></li><li class="chapter-item expanded "><a href="../approved/0032-minimal-relay.html">RFC-0032: Minimal Relay</a></li><li class="chapter-item expanded "><a href="../approved/0042-extrinsics-state-version.html">RFC-0042: Add System version that replaces StateVersion on RuntimeVersion</a></li><li class="chapter-item expanded "><a href="../approved/0043-storage-proof-size-hostfunction.html">RFC-0043: Introduce storage_proof_size Host Function for Improved Parachain Block Utilization</a></li><li class="chapter-item expanded "><a href="../approved/0045-nft-deposits-asset-hub.html">RFC-0045: Lowering NFT Deposits on Asset Hub</a></li><li class="chapter-item expanded "><a href="../approved/0047-assignment-of-availability-chunks.html">RFC-0047: Assignment of availability chunks to validators</a></li><li class="chapter-item expanded "><a href="../approved/0048-session-keys-runtime-api.html">RFC-0048: Generate ownership proof for SessionKeys</a></li><li class="chapter-item expanded "><a href="../approved/0050-fellowship-salaries.html">RFC-0050: Fellowship Salaries</a></li><li class="chapter-item expanded "><a href="../approved/0056-one-transaction-per-notification.html">RFC-0056: Enforce only one transaction per notification</a></li><li class="chapter-item expanded "><a href="../approved/0059-nodes-capabilities-discovery.html">RFC-0059: Add a discovery mechanism for nodes based on their capabilities</a></li><li class="chapter-item expanded "><a href="../approved/0078-merkleized-metadata.html">RFC-0078: Merkleized Metadata</a></li><li class="chapter-item expanded "><a href="../approved/0084-general-transaction-extrinsic-format.html">RFC-0084: General transactions in extrinsic format</a></li><li class="chapter-item expanded "><a href="../approved/0091-dht-record-creation-time.html">RFC-0091: DHT Authority discovery record creation time</a></li><li class="chapter-item expanded "><a href="../approved/0097-unbonding_queue.html">RFC-0097: Unbonding Queue</a></li><li class="chapter-item expanded "><a href="../approved/0099-transaction-extension-version.html">RFC-0099: Introduce a transaction extension version</a></li><li class="chapter-item expanded "><a href="../approved/0100-xcm-multi-type-asset-transfer.html">RFC-0100: New XCM instruction: InitiateAssetsTransfer</a></li><li class="chapter-item expanded "><a href="../approved/0101-xcm-transact-remove-max-weight-param.html">RFC-0101: XCM Transact remove require_weight_at_most parameter</a></li><li class="chapter-item expanded "><a href="../approved/0103-introduce-core-index-commitment.html">RFC-0103: Introduce a CoreIndex commitment and a SessionIndex field in candidate receipts</a></li><li class="chapter-item expanded "><a href="../approved/0105-xcm-improved-fee-mechanism.html">RFC-0105: XCM improved fee mechanism</a></li><li class="chapter-item expanded "><a href="../approved/0107-xcm-execution-hints.html">RFC-0107: XCM Execution hints</a></li><li class="chapter-item expanded "><a href="../approved/0108-xcm-remove-testnet-ids.html">RFC-0108: Remove XCM testnet NetworkIds</a></li><li class="chapter-item expanded "><a href="../approved/0122-alias-origin-on-asset-transfers.html">RFC-0122: Asset transfers can alias XCM origin on destination to original origin</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Stale</li><li class="chapter-item expanded "><a href="../stale/0000-rewards.html">RFC-0000: Validator Rewards</a></li><li class="chapter-item expanded "><a href="../stale/0006-dynamic-pricing-for-bulk-coretime-sales.html">RFC-0006: Dynamic Pricing for Bulk Coretime Sales</a></li><li class="chapter-item expanded "><a href="../stale/0015-market-design-revisit.html">RFC-0015: Market Design Revisit</a></li><li class="chapter-item expanded "><a href="../stale/0034-xcm-absolute-location-account-derivation.html">RFC-34: XCM Absolute Location Account Derivation</a></li><li class="chapter-item expanded "><a href="../stale/0035-conviction-voting-delegation-modifications.html"> RFC-0035: Conviction Voting Delegation Modifications</a></li><li class="chapter-item expanded "><a href="../stale/0044-rent-based-registration.html">RFC-0044: Rent based registration model</a></li><li class="chapter-item expanded "><a href="../stale/0054-remove-heap-pages.html">RFC-0054: Remove the concept of "heap pages" from the client</a></li><li class="chapter-item expanded "><a href="../stale/0070-x-track-kusamanetwork.html">RFC-0070: X Track for @kusamanetwork</a></li><li class="chapter-item expanded "><a href="../stale/0073-referedum-deposit-track.html">RFC-0073: Decision Deposit Referendum Track</a></li><li class="chapter-item expanded "><a href="../stale/0074-stateful-multisig-pallet.html" class="active">RFC-0074: Stateful Multisig Pallet</a></li><li class="chapter-item expanded "><a href="../stale/0077-increase-max-length-of-identity-pgp-fingerprint-value.html">RFC-0077: Increase maximum length of identity PGP fingerprint values from 20 bytes</a></li><li class="chapter-item expanded "><a href="../stale/0088-broker-pallet-slashable-deposit-purchaser-reputation-reserved-cores.html">RFC-0088: Add slashable locked deposit, purchaser reputation, and reserved cores for on-chain identities to broker pallet</a></li><li class="chapter-item expanded "><a href="../stale/00xx-secondary-marketplace-for-regions.html">RFC-0001: Secondary Market for Regions</a></li><li class="chapter-item expanded "><a href="../stale/00xx-smart-contracts-coretime-chain.html">RFC-0002: Smart Contracts on the Coretime Chain</a></li><li class="chapter-item expanded "><a href="../stale/0102-offchain-parachain-runtime-upgrades.html">RFC-0000: Feature Name Here</a></li><li class="chapter-item expanded "><a href="../stale/0106-xcm-remove-fees-mode.html">RFC-0106: Remove XCM fees mode</a></li><li class="chapter-item expanded "><a href="../stale/0111-pure-proxy-replication.html">RFC-0111: Pure Proxy Replication</a></li><li class="chapter-item expanded "><a href="../stale/0112-compress-state-response-message-in-state-sync.html">RFC-0112: Compress the State Response Message in State Sync</a></li><li class="chapter-item expanded "><a href="../stale/0114-secp256r1-hostfunction.html">RFC-0114: Introduce secp256r1_ecdsa_verify_prehashed Host Function to verify NIST-P256 elliptic curve signatures</a></li><li class="chapter-item expanded "><a href="../stale/0117-unbrick-collective.html">RFC-0117: The Unbrick Collective</a></li><li class="chapter-item expanded "><a href="../stale/0120-referenda-confirmation-by-candle-mechanism.html">RFC-0120: Referenda Confirmation by Candle Mechanism</a></li><li class="chapter-item expanded "><a href="../stale/0121-iterable-referenda-tracks.html">RFC-0121: Iterable Referenda Tracks</a></li><li class="chapter-item expanded "><a href="../stale/0124-extrinsic-version-5.html">RFC-0124: Extrinsic version 5</a></li><li class="chapter-item expanded "><a href="../stale/RFC-114 Adjust Tipper Track Confirmation Periods.html">RFC-114: Adjust Tipper Track Confirmation Periods</a></li><li class="chapter-item expanded "><a href="../stale/TODO-stale-nomination-reward-curve.html">RFC-TODO: Stale Nomination Reward Curve</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="polkadot">Polkadot</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Polkadot Fellowship RFCs</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<p><a href="https://github.com/polkadot-fellows/RFCs/pull/74">(source)</a></p>
<p><strong>Table of Contents</strong></p>
<ul>
<li><a href="#rfc-0074-stateful-multisig-pallet">RFC-0074: Stateful Multisig Pallet</a>
<ul>
<li><a href="#summary">Summary</a></li>
<li><a href="#motivation">Motivation</a>
<ul>
<li><a href="#problem">Problem</a></li>
<li><a href="#requirements">Requirements</a></li>
<li><a href="#use-cases">Use Cases</a></li>
</ul>
</li>
<li><a href="#stakeholders">Stakeholders</a></li>
<li><a href="#explanation">Explanation</a>
<ul>
<li><a href="#state-transition-functions">State Transition Functions</a></li>
<li><a href="#storagestate">Storage/State</a></li>
<li><a href="#considerations--edge-cases">Considerations &amp; Edge cases</a></li>
</ul>
</li>
<li><a href="#drawbacks">Drawbacks</a></li>
<li><a href="#testing-security-and-privacy">Testing, Security, and Privacy</a></li>
<li><a href="#performance-ergonomics-and-compatibility">Performance, Ergonomics, and Compatibility</a>
<ul>
<li><a href="#performance">Performance</a></li>
<li><a href="#ergonomics">Ergonomics</a></li>
<li><a href="#compatibility">Compatibility</a></li>
</ul>
</li>
<li><a href="#prior-art-and-references">Prior Art and References</a></li>
<li><a href="#unresolved-questions">Unresolved Questions</a></li>
<li><a href="#future-directions-and-related-material">Future Directions and Related Material</a></li>
</ul>
</li>
</ul>
<h1 id="rfc-0074-stateful-multisig-pallet"><a class="header" href="#rfc-0074-stateful-multisig-pallet">RFC-0074: Stateful Multisig Pallet</a></h1>
<div class="table-wrapper"><table><thead><tr><th></th><th></th></tr></thead><tbody>
<tr><td><strong>Start Date</strong></td><td>15 February 2024</td></tr>
<tr><td><strong>Description</strong></td><td>Add Enhanced Multisig Pallet to System chains</td></tr>
<tr><td><strong>Authors</strong></td><td>Abdelrahman Soliman (Boda)</td></tr>
</tbody></table>
</div>
<h2 id="summary"><a class="header" href="#summary">Summary</a></h2>
<p>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 <strong>not</strong> intended for migrating or getting rid of existing multisig. It's to allow both options to coexist.</p>
<p>For the rest of the RFC We use the following terms:</p>
<ul>
<li><code>proposal</code> to refer to an extrinsic that is to be dispatched from a multisig account after getting enough approvals.</li>
<li><code>Stateful Multisig</code> to refer to the proposed pallet.</li>
<li><code>Stateless Multisig</code> to refer to the current multisig pallet in polkadot-sdk.</li>
</ul>
<h2 id="motivation"><a class="header" href="#motivation">Motivation</a></h2>
<h3 id="problem"><a class="header" href="#problem">Problem</a></h3>
<p>Entities in the Polkadot ecosystem need to have a way to manage their funds and other operations in a secure and efficient way. Multisig accounts are a common way to achieve this. Entities by definition change over time, members of the entity may change, threshold requirements may change, and the multisig account may need to be deleted. For even more enhanced hierarchical control, the multisig account may need to be controlled by other multisig accounts.</p>
<p>Current native solutions for multisig operations are less optimal, performance-wise (as we'll explain later in the RFC), and lack fine-grained control over the multisig account.</p>
<h4 id="stateless-multisig"><a class="header" href="#stateless-multisig">Stateless Multisig</a></h4>
<p>We refer to current <a href="https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/multisig">multisig pallet in polkadot-sdk</a> because the multisig account is only derived and not stored in the state. Although deriving the account is determinsitc as it relies on exact users (sorted) and thershold to derive it. This does not allow for control over the multisig account. It's also tightly coupled to exact users and threshold. This makes it hard for an organization to manage existing accounts and to change the threshold or add/remove signers.</p>
<p>We believe as well that the stateless multisig is not efficient in terms of block footprint as we'll show in the performance section.</p>
<h4 id="pure-proxy"><a class="header" href="#pure-proxy">Pure Proxy</a></h4>
<p>Pure proxy can achieve having a stored and determinstic multisig account from different users but it's unneeded complexity as a way around the limitations of the current multisig pallet. It doesn't also have the same fine grained control over the multisig account.</p>
<p>Other points mentioned by @tbaut</p>
<ul>
<li>pure proxies aren't (yet) a thing cross chain</li>
<li>the end user complexity is much much higher with pure proxies, also for new users smart contract multisig are widely known while pure proxies are obscure.</li>
<li>you can shoot yourself in the foot by deleting the proxy, and effectively loosing access to funds with pure proxies.</li>
</ul>
<h3 id="requirements"><a class="header" href="#requirements">Requirements</a></h3>
<p>Basic requirements for the Stateful Multisig are:</p>
<ul>
<li>The ability to have concrete and permanent (unless deleted) multisig accounts in the state.</li>
<li>The ability to add/remove signers from an existing multisig account by the multisig itself.</li>
<li>The ability to change the threshold of an existing multisig account by the multisig itself.</li>
<li>The ability to delete an existing multisig account by the multisig itself.</li>
<li>The ability to cancel an existing proposal by the multisig itself.</li>
<li>Signers of multisig account can start a proposal on behalf of the multisig account which will be dispatched after getting enough approvals.</li>
<li>Signers of multisig account can approve/reject a proposal while still exists.</li>
</ul>
<h3 id="use-cases"><a class="header" href="#use-cases">Use Cases</a></h3>
<ul>
<li>
<p>Corporate Governance:
In a corporate setting, multisig accounts can be employed for decision-making processes. For example, a company may require the approval of multiple executives to initiate significant financial transactions.</p>
</li>
<li>
<p>Joint Accounts:
Multisig accounts can be used for joint accounts where multiple individuals need to authorize transactions. This is particularly useful in family finances or shared business accounts.</p>
</li>
<li>
<p>Decentralized Autonomous Organizations (DAOs):
DAOs can utilize multisig accounts to ensure that decisions are made collectively. Multiple key holders can be required to approve changes to the organization's rules or the allocation of funds.</p>
</li>
</ul>
<p>and much more...</p>
<h2 id="stakeholders"><a class="header" href="#stakeholders">Stakeholders</a></h2>
<ul>
<li>Polkadot holders</li>
<li>Polkadot developers</li>
</ul>
<h2 id="explanation"><a class="header" href="#explanation">Explanation</a></h2>
<p>I've created the stateful multisig pallet during my studies in Polkadot Blockchain Academy under supervision from @shawntabrizi and @ank4n. After that, I've enhanced it to be fully functional and this is a draft <a href="https://github.com/paritytech/polkadot-sdk/pull/3300">PR#3300</a> in polkadot-sdk. I'll list all the details and design decisions in the following sections. Note that the PR is not 1-1 exactly to the current RFC as the RFC is a more polished version of the PR after updating based on the feedback and discussions.</p>
<p>Let's start with a sequence diagram to illustrate the main operations of the Stateful Multisig.</p>
<p><img src="https://github.com/asoliman92/RFCs/assets/2677789/4f2e8972-f3b8-4250-b75f-1e4788b35752" alt="multisig operations" /></p>
<p>Notes on above diagram:</p>
<ul>
<li>It's a 3 step process to execute a proposal. (Start Proposal --&gt; Approvals --&gt; Execute Proposal)</li>
<li><code>Execute</code> is an explicit extrinsic for a simpler API. It can be optimized to be executed automatically after getting enough approvals.</li>
<li>Any user can create a multisig account and they don't need to be part of it. (Alice in the diagram)</li>
<li>A proposal is any extrinsic including control extrinsics (e.g. add/remove signer, change threshold,..etc).</li>
<li>Any multisig account signer can start a proposal on behalf of the multisig account. (Bob in the diagram)</li>
<li>Any multisig account owener can execute proposal if it's approved by enough signers. (Dave in the diagram)</li>
</ul>
<h3 id="state-transition-functions"><a class="header" href="#state-transition-functions">State Transition Functions</a></h3>
<p>having the following enum to store the call or the hash:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>enum CallOrHash&lt;T: Config&gt; {
Call(&lt;T as Config&gt;::RuntimeCall),
Hash(T::Hash),
}
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>create_multisig</code> - Create a multisig account with a given threshold and initial signers. (Needs Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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&lt;T&gt;,
signers: BoundedBTreeSet&lt;T::AccountId, T::MaxSignatories&gt;,
threshold: u32,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>start_proposal</code> - Start a multisig proposal. (Needs Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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_or_hash` - The enum having the call or the hash of the call to be approved and executed later.
///
/// # 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&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>approve</code> - Approve a multisig proposal.</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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 -&gt; reject -&gt; approve, the proposal will be approved.
/// If a signer did approve -&gt; reject, the proposal will be rejected.
///
/// # Arguments
///
/// * `multisig_account` - The multisig account ID.
/// * `call_or_hash` - The enum having the call or the hash of the call to be approved.
///
/// # 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&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>reject</code> - Reject a multisig proposal.</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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 -&gt; reject -&gt; approve, the proposal will be approved.
/// If a signer did approve -&gt; reject, the proposal will be rejected.
///
/// # Arguments
///
/// * `multisig_account` - The multisig account ID.
/// * `call_or_hash` - The enum having the call or the hash of the call to be rejected.
///
/// # 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&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>execute_proposal</code> - Execute a multisig proposal. (Releases Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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_or_hash` - We should have gotten the RuntimeCall (preimage) and stored it in the proposal by the time the extrinsic is called.
///
/// # 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.
/// * `ProposalNotFound` - The proposal does not exist.
/// * `CallPreImageNotFound` - The proposal doesn't have the preimage of the call in the state.
pub fn execute_proposal(
origin: OriginFor&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>cancel_proposal</code> - Cancel a multisig proposal. (Releases Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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_or_hash` - The call or hash of the call to be canceled.
///
/// # Errors
///
/// * `MultisigNotFound` - The multisig account does not exist.
/// * `ProposalNotFound` - The proposal does not exist.
pub fn cancel_proposal(
origin: OriginFor&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>cancel_own_proposal</code> - Cancel a multisig proposal started by the caller in case no other signers approved it yet. (Releases Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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_or_hash` - The hash of the call to be canceled.
///
/// # Errors
///
/// * `MultisigNotFound` - The multisig account does not exist.
/// * `ProposalNotFound` - The proposal does not exist.
pub fn cancel_own_proposal(
origin: OriginFor&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>cleanup_proposals</code> - Cleanup proposals of a multisig account. (Releases Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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&lt;T&gt;,
multisig_account: T::AccountId,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<p>Note: Next functions need to be called from the multisig account itself. Deposits are reserved from the multisig account as well.</p>
<ul>
<li><code>add_signer</code> - Add a new signer to a multisig account. (Needs Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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&lt;T&gt;,
new_signer: T::AccountId,
new_threshold: u32,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>remove_signer</code> - Remove an signer from a multisig account. (Releases Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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&lt;T&gt;,
signer_to_remove: T::AccountId,
new_threshold: u32,
) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>set_threshold</code> - Change the threshold of a multisig account.</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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&lt;T&gt;, new_threshold: u32) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>delete_multisig</code> - Delete a multisig account. (Releases Deposit)</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span> /// 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&lt;T&gt;) -&gt; DispatchResult
<span class="boring">}</span></code></pre></pre>
<h3 id="storagestate"><a class="header" href="#storagestate">Storage/State</a></h3>
<ul>
<li>Use 2 main storage maps to store mutlisig accounts and proposals.</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>#[pallet::storage]
pub type MultisigAccount&lt;T: Config&gt; = StorageMap&lt;_, Twox64Concat, T::AccountId, MultisigAccountDetails&lt;T&gt;&gt;;
/// The set of open multisig proposals. A proposal is uniquely identified by the multisig account and the call hash.
/// (maybe a nonce as well in the future)
#[pallet::storage]
pub type PendingProposals&lt;T: Config&gt; = StorageDoubleMap&lt;
_,
Twox64Concat,
T::AccountId, // Multisig Account
Blake2_128Concat,
T::Hash, // Call Hash
MultisigProposal&lt;T&gt;,
&gt;;
<span class="boring">}</span></code></pre></pre>
<p>As for the values:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct MultisigAccountDetails&lt;T: Config&gt; {
/// The signers of the multisig account. This is a BoundedBTreeSet to ensure faster operations (add, remove).
/// As well as lookups and faster set operations to ensure approvers is always a subset from signers. (e.g. in case of removal of an signer during an active proposal)
pub signers: BoundedBTreeSet&lt;T::AccountId, T::MaxSignatories&gt;,
/// The threshold of approvers required for the multisig account to be able to execute a call.
pub threshold: u32,
pub deposit: BalanceOf&lt;T&gt;,
}
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct MultisigProposal&lt;T: Config&gt; {
/// Proposal creator.
pub creator: T::AccountId,
pub creation_deposit: BalanceOf&lt;T&gt;,
/// The extrinsic when the multisig operation was opened.
pub when: Timepoint&lt;BlockNumberFor&lt;T&gt;&gt;,
/// The approvers achieved so far, including the depositor.
/// The approvers are stored in a BoundedBTreeSet to ensure faster lookup and operations (approve, reject).
/// It's also bounded to ensure that the size don't go over the required limit by the Runtime.
pub approvers: BoundedBTreeSet&lt;T::AccountId, T::MaxSignatories&gt;,
/// The rejectors for the proposal so far.
/// The rejectors are stored in a BoundedBTreeSet to ensure faster lookup and operations (approve, reject).
/// It's also bounded to ensure that the size don't go over the required limit by the Runtime.
pub rejectors: BoundedBTreeSet&lt;T::AccountId, T::MaxSignatories&gt;,
/// The block number until which this multisig operation is valid. None means no expiry.
pub expire_after: Option&lt;BlockNumberFor&lt;T&gt;&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>For optimization we're using BoundedBTreeSet to allow for efficient lookups and removals. Especially in the case of approvers, we need to be able to remove an approver from the list when they reject their approval. (which we do lazily when <code>execute_proposal</code> is called).</p>
<p>There's an extra storage map for the deposits of the multisig accounts per signer added. This is to ensure that we can release the deposits when the multisig removes them even if the constant deposit per signer changed in the runtime later on.</p>
<h3 id="considerations--edge-cases"><a class="header" href="#considerations--edge-cases">Considerations &amp; Edge cases</a></h3>
<h4 id="removing-an-signer-from-the-multisig-account-during-an-active-proposal"><a class="header" href="#removing-an-signer-from-the-multisig-account-during-an-active-proposal">Removing an signer from the multisig account during an active proposal</a></h4>
<p>We need to ensure that the approvers are always a subset from signers. This is also partially why we're using BoundedBTreeSet for signers and approvers. Once execute proposal is called we ensure that the proposal is still valid and the approvers are still a subset from current signers.</p>
<h4 id="multisig-account-deletion-and-cleaning-up-existing-proposals"><a class="header" href="#multisig-account-deletion-and-cleaning-up-existing-proposals">Multisig account deletion and cleaning up existing proposals</a></h4>
<p>Once the last signer of a multisig account is removed or the multisig approved the account deletion we delete the multisig accound from the state and keep the proposals until someone calls <code>cleanup_proposals</code> multiple times which iterates over a max limit per extrinsic. This is to ensure we don't have unbounded iteration over the proposals. Users are already incentivized to call <code>cleanup_proposals</code> to get their deposits back.</p>
<h4 id="multisig-account-deletion-and-existing-deposits"><a class="header" href="#multisig-account-deletion-and-existing-deposits">Multisig account deletion and existing deposits</a></h4>
<p>We currently just delete the account without checking for deposits (Would like to hear your thoughts here). We can either</p>
<ul>
<li>Don't make deposits to begin with and make it a fee.</li>
<li>Transfer to treasury.</li>
<li>Error on deletion. (don't like this)</li>
</ul>
<h4 id="approving-a-proposal-after-the-threshold-is-changed"><a class="header" href="#approving-a-proposal-after-the-threshold-is-changed">Approving a proposal after the threshold is changed</a></h4>
<p>We always use latest threshold and don't store each proposal with different threshold. This allows the following:</p>
<ul>
<li>In case threshold is lower than the number of approvers then the proposal is still valid.</li>
<li>In case threshold is higher than the number of approvers then we catch it during execute proposal and error.</li>
</ul>
<h2 id="drawbacks"><a class="header" href="#drawbacks">Drawbacks</a></h2>
<ul>
<li>New pallet to maintain.</li>
</ul>
<h2 id="testing-security-and-privacy"><a class="header" href="#testing-security-and-privacy">Testing, Security, and Privacy</a></h2>
<p>Standard audit/review requirements apply.</p>
<h2 id="performance-ergonomics-and-compatibility"><a class="header" href="#performance-ergonomics-and-compatibility">Performance, Ergonomics, and Compatibility</a></h2>
<h3 id="performance"><a class="header" href="#performance">Performance</a></h3>
<p>Doing back of the envelop calculation to proof that the stateful multisig is more efficient than the stateless multisig given it's smaller footprint size on blocks.</p>
<p>Quick review over the extrinsics for both as it affects the block size:</p>
<p>Stateless Multisig:
Both <code>as_multi</code> and <code>approve_as_multi</code> has a similar parameters:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>origin: OriginFor&lt;T&gt;,
threshold: u16,
other_signatories: Vec&lt;T::AccountId&gt;,
maybe_timepoint: Option&lt;Timepoint&lt;BlockNumberFor&lt;T&gt;&gt;&gt;,
call_hash: [u8; 32],
max_weight: Weight,
<span class="boring">}</span></code></pre></pre>
<p>Stateful Multisig:
We have the following extrinsics:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn start_proposal(
origin: OriginFor&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
)
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn approve(
origin: OriginFor&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
)
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn execute_proposal(
origin: OriginFor&lt;T&gt;,
multisig_account: T::AccountId,
call_or_hash: CallOrHash,
)
<span class="boring">}</span></code></pre></pre>
<p>The main takeway is that we don't need to pass the threshold and other signatories in the extrinsics. This is because we already have the threshold and signatories in the state (only once).</p>
<p>So now for the caclulations, given the following:</p>
<ul>
<li>K is the number of multisig accounts.</li>
<li>N is number of signers in each multisig account.</li>
<li>For each proposal we need to have 2N/3 approvals.</li>
</ul>
<p>The table calculates if each of the K multisig accounts has one proposal and it gets approved by the 2N/3 and then executed. How much did the total Blocks and States sizes increased by the end of the day.</p>
<p>Note: We're not calculating the cost of proposal as both in statefull and stateless multisig they're almost the same and gets cleaned up from the state once the proposal is executed or canceled.</p>
<p>Stateless effect on blocksizes = 2/3<em>K</em>N^2 (as each user of the 2/3 users will need to call approve_as_multi with all the other signatories(N) in extrinsic body)</p>
<p>Stateful effect on blocksizes = K * N (as each user will need to call approve with the multisig account only in extrinsic body)</p>
<p>Stateless effect on statesizes = Nil (as the multisig account is not stored in the state)</p>
<p>Stateful effect on statesizes = K*N (as each multisig account (K) will be stored with all the signers (K) in the state)</p>
<div class="table-wrapper"><table><thead><tr><th>Pallet</th><th style="text-align: center">Block Size</th><th style="text-align: right">State Size</th></tr></thead><tbody>
<tr><td>Stateless</td><td style="text-align: center">2/3<em>K</em>N^2</td><td style="text-align: right">Nil</td></tr>
<tr><td>Stateful</td><td style="text-align: center">K*N</td><td style="text-align: right">K*N</td></tr>
</tbody></table>
</div>
<p>Simplified table removing K from the equation:
| Pallet | Block Size | State Size |
|----------------|:-------------:|-----------:|
| Stateless | N^2 | Nil |
| Stateful | N | N |</p>
<p>So even though the stateful multisig has a larger state size, it's still more efficient in terms of block size and total footprint on the blockchain.</p>
<h3 id="ergonomics"><a class="header" href="#ergonomics">Ergonomics</a></h3>
<p>The Stateful Multisig will have better ergonomics for managing multisig accounts for both developers and end-users.</p>
<h3 id="compatibility"><a class="header" href="#compatibility">Compatibility</a></h3>
<p>This RFC is compatible with the existing implementation and can be handled via upgrades and migration. It's not intended to replace the existing multisig pallet.</p>
<h2 id="prior-art-and-references"><a class="header" href="#prior-art-and-references">Prior Art and References</a></h2>
<p><a href="https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/multisig">multisig pallet in polkadot-sdk</a></p>
<h2 id="unresolved-questions"><a class="header" href="#unresolved-questions">Unresolved Questions</a></h2>
<ul>
<li>On account deletion, should we transfer remaining deposits to treasury or remove signers' addition deposits completely and consider it as fees to start with?</li>
</ul>
<h2 id="future-directions-and-related-material"><a class="header" href="#future-directions-and-related-material">Future Directions and Related Material</a></h2>
<ul>
<li><input disabled="" type="checkbox"/>
Batch addition/removal of signers.</li>
<li><input disabled="" type="checkbox"/>
Add expiry to proposals. After a certain time, proposals will not accept any more approvals or executions and will be deleted.</li>
<li><input disabled="" type="checkbox"/>
Implement call filters. This will allow multisig accounts to only accept certain calls.</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../stale/0073-referedum-deposit-track.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../stale/0077-increase-max-length-of-identity-pgp-fingerprint-value.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../stale/0073-referedum-deposit-track.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../stale/0077-increase-max-length-of-identity-pgp-fingerprint-value.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>