Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions prdoc/pr_12711.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title: 'pallet-multisig: add try_state invariant checks'

doc:
- audience: Runtime Dev
description: |-
Adds a `try_state` hook to `pallet-multisig` (issue #239), run under the `try-runtime` feature and after every unit test. For each open operation it asserts that the approvals list is non empty, strictly sorted and duplicate free, and contains the depositor, and that every depositor's reserved balance covers the sum of its multisig deposits. The stored per-operation deposit is intentionally not asserted against the current deposit parameters, since it goes stale until poked when those parameters change. The change is additive with no migration or weight changes.

crates:
- name: pallet-multisig
bump: minor
48 changes: 47 additions & 1 deletion substrate/frame/multisig/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ pub use weights::WeightInfo;
/// Re-export all pallet items.
pub use pallet::*;

#[cfg(feature = "try-runtime")]
use frame::try_runtime::TryRuntimeError;

/// The log target of this pallet.
pub const LOG_TARGET: &'static str = "runtime::multisig";

Expand Down Expand Up @@ -292,7 +295,14 @@ pub mod pallet {
}

#[pallet::hooks]
impl<T: Config> Hooks<frame_system::pallet_prelude::BlockNumberFor<T>> for Pallet<T> {}
impl<T: Config> Hooks<frame_system::pallet_prelude::BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "try-runtime")]
fn try_state(
_n: frame_system::pallet_prelude::BlockNumberFor<T>,
) -> Result<(), TryRuntimeError> {
Self::do_try_state()
}
}

#[pallet::call]
impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -815,6 +825,42 @@ impl<T: Config> Pallet<T> {
}
}

#[cfg(feature = "try-runtime")]
impl<T: Config> Pallet<T> {
/// Invariants that must hold before and after every state transition of this pallet.
///
/// Deposits are checked via the total reserved per depositor, not against `Deposit*` params,
/// which go stale until poked when changed.
pub fn do_try_state() -> Result<(), TryRuntimeError> {
use alloc::collections::btree_map::BTreeMap;

let mut reserved_by_depositor: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
for (_id, _call_hash, m) in Multisigs::<T>::iter() {
ensure!(!m.approvals.is_empty(), "Multisig must have at least one approval");
ensure!(
m.approvals.windows(2).all(|w| w[0] < w[1]),
"Multisig approvals must be strictly sorted and duplicate-free"
);
ensure!(
m.approvals.contains(&m.depositor),
"Multisig depositor must be among the approvals"
);

let entry = reserved_by_depositor.entry(m.depositor).or_default();
*entry = entry.saturating_add(m.deposit);
}

for (depositor, reserved) in reserved_by_depositor {
ensure!(
T::Currency::reserved_balance(&depositor) >= reserved,
"Depositor's reserved balance must cover its multisig deposits"
);
}

Ok(())
}
}

/// Return the weight of a dispatch call result as an `Option`.
///
/// Will return the weight regardless of what the state of the result is.
Expand Down
Loading
Loading