feat: Add SafeMode and TxPause Pallets (#192)

### Overview
This PR integrates the `pallet-safe-mode` and `pallet-tx-pause` from
Polkadot SDK to provide comprehensive emergency governance controls
across all DataHaven runtime networks (mainnet, stagenet, testnet).

### Key Changes

#### 🔧 **Core Integration**
- **Dependencies**: Added `pallet-safe-mode` and `pallet-tx-pause` from
`polkadot-stable2412-6`
- **Runtime Integration**: Integrated both pallets across all three
runtime networks with pallet indices 103 and 104
- **Call Filtering**: Implemented unified `RuntimeCallFilter` that
combines Normal, SafeMode, and TxPause restrictions

#### 🛡️ **SafeMode Pallet Configuration**
- **Duration**: 1 day activation period (`DAYS` constant)
- **Deposits**: Disabled permissionless entry/extension (all `None`)
- **Origins**: Root-only for all force operations (`force_enter`,
`force_exit`, `force_extend`, etc.)
- **Whitelisting**: SafeMode and Sudo calls are immune to restrictions

#### ⏸️ **TxPause Pallet Configuration** 
- **Origins**: Root-only pause/unpause control
- **Whitelisting**: SafeMode and Sudo calls cannot be paused
- **Max Call Name Length**: 256 characters

#### 🏗️ **Architecture**
- **Shared Types**: Created `operator/runtime/common/src/safe_mode.rs`
with reusable configurations
- **Combined Filtering**: `RuntimeCallFilter` applies all three filter
layers (Normal + SafeMode + TxPause)
- **Consistent Config**: Identical configuration across mainnet,
stagenet, and testnet

#### 📊 **Infrastructure Updates**
- **Benchmarking**: Added both pallets to benchmark suites across all
networks
- **Weight Mappings**: Placeholder weights using Substrate defaults
(ready for chain-specific benchmarking)
- **Metadata**: Updated runtime metadata for new pallet exposure

#### 🧪 **Testing Framework**
- **Coverage**: Tests for individual pallet behavior, combined
restrictions, whitelisting, and edge cases

### Emergency Control Capabilities

**SafeMode Pallet** (8 calls):
- User calls: `enter`, `extend`, `release_deposit`
- Force calls: `force_enter`, `force_exit`, `force_extend`,
`force_slash_deposit`, `force_release_deposit`

**TxPause Pallet** (2 calls):
- `pause_call` / `unpause_call` - Granular transaction type pausing

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
This commit is contained in:
Ahmad Kaouk 2025-10-06 19:00:10 +02:00 committed by GitHub
parent b4f697f954
commit ac09a4f2bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1834 additions and 13 deletions

43
operator/Cargo.lock generated
View file

@ -2892,6 +2892,7 @@ dependencies = [
"pallet-proxy",
"pallet-randomness",
"pallet-referenda",
"pallet-safe-mode",
"pallet-scheduler",
"pallet-session",
"pallet-storage-providers",
@ -2901,6 +2902,7 @@ dependencies = [
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
"pallet-treasury",
"pallet-tx-pause",
"pallet-utility",
"pallet-whitelist",
"parity-scale-codec",
@ -3081,7 +3083,9 @@ dependencies = [
"pallet-evm",
"pallet-evm-precompile-proxy",
"pallet-migrations",
"pallet-safe-mode",
"pallet-treasury",
"pallet-tx-pause",
"parity-scale-codec",
"polkadot-primitives",
"polkadot-runtime-common",
@ -3166,6 +3170,7 @@ dependencies = [
"pallet-proxy",
"pallet-randomness",
"pallet-referenda",
"pallet-safe-mode",
"pallet-scheduler",
"pallet-session",
"pallet-storage-providers",
@ -3175,6 +3180,7 @@ dependencies = [
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
"pallet-treasury",
"pallet-tx-pause",
"pallet-utility",
"pallet-whitelist",
"parity-scale-codec",
@ -3306,6 +3312,7 @@ dependencies = [
"pallet-proxy",
"pallet-randomness",
"pallet-referenda",
"pallet-safe-mode",
"pallet-scheduler",
"pallet-session",
"pallet-storage-providers",
@ -3315,6 +3322,7 @@ dependencies = [
"pallet-transaction-payment",
"pallet-transaction-payment-rpc-runtime-api",
"pallet-treasury",
"pallet-tx-pause",
"pallet-utility",
"pallet-whitelist",
"parity-scale-codec",
@ -9589,6 +9597,24 @@ dependencies = [
"sp-runtime",
]
[[package]]
name = "pallet-safe-mode"
version = "20.0.0"
source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2412-6#bbc435c7667d3283ba280a8fec44676357392753"
dependencies = [
"docify",
"frame-benchmarking",
"frame-support",
"frame-system",
"pallet-balances",
"pallet-proxy",
"pallet-utility",
"parity-scale-codec",
"scale-info",
"sp-arithmetic",
"sp-runtime",
]
[[package]]
name = "pallet-scheduler"
version = "40.2.0"
@ -9789,6 +9815,23 @@ dependencies = [
"sp-runtime",
]
[[package]]
name = "pallet-tx-pause"
version = "20.0.0"
source = "git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2412-6#bbc435c7667d3283ba280a8fec44676357392753"
dependencies = [
"docify",
"frame-benchmarking",
"frame-support",
"frame-system",
"pallet-balances",
"pallet-proxy",
"pallet-utility",
"parity-scale-codec",
"scale-info",
"sp-runtime",
]
[[package]]
name = "pallet-utility"
version = "39.1.0"

View file

@ -121,6 +121,8 @@ pallet-multisig = { git = "https://github.com/paritytech/polkadot-sdk", tag = "p
pallet-offences = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
pallet-parameters = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
pallet-preimage = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
pallet-safe-mode = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
pallet-tx-pause = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
pallet-collator-selection = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2412-6", default-features = false }
pallet-collective = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }
pallet-conviction-voting = { git = "https://github.com/paritytech/polkadot-sdk", tag = "polkadot-stable2412-6", default-features = false }

View file

@ -14,6 +14,8 @@ pallet-balances = { workspace = true }
pallet-evm = { workspace = true }
pallet-evm-precompile-proxy = { workspace = true }
pallet-migrations = { workspace = true }
pallet-safe-mode = { workspace = true }
pallet-tx-pause = { workspace = true }
pallet-treasury = { workspace = true }
polkadot-primitives = { workspace = true }
polkadot-runtime-common = { workspace = true }
@ -34,6 +36,8 @@ std = [
"pallet-evm/std",
"pallet-evm-precompile-proxy/std",
"pallet-migrations/std",
"pallet-safe-mode/std",
"pallet-tx-pause/std",
"pallet-treasury/std",
"polkadot-primitives/std",
"polkadot-runtime-common/std",
@ -48,6 +52,8 @@ std = [
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"pallet-migrations/runtime-benchmarks",
"pallet-safe-mode/runtime-benchmarks",
"pallet-tx-pause/runtime-benchmarks",
"polkadot-primitives/runtime-benchmarks",
"polkadot-runtime-common/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",

View file

@ -24,6 +24,8 @@ pub mod deal_with_fees;
pub mod impl_on_charge_evm_transaction;
pub mod migrations;
pub use migrations::*;
pub mod safe_mode;
pub use safe_mode::*;
use fp_account::EthereumSignature;
pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic;

View file

@ -0,0 +1,73 @@
// Copyright 2019-2025 DataHaven Inc.
// This file is part of DataHaven.
// Moonbeam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// DataHaven is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Moonbeam. If not, see <http://www.gnu.org/licenses/>.
//! Safe Mode and Tx Pause shared types, constants, and utilities
use crate::time::DAYS;
use crate::Balance;
use frame_support::{parameter_types, traits::Contains};
use pallet_tx_pause::RuntimeCallNameOf;
use polkadot_primitives::BlockNumber;
use sp_std::marker::PhantomData;
// Safe Mode Constants
parameter_types! {
/// Default duration for safe mode activation (1 day)
pub const SafeModeDuration: BlockNumber = DAYS;
pub const SafeModeEnterDeposit: Option<Balance> = None;
/// Safe mode extend deposit - None disables permissionless extend
pub const SafeModeExtendDeposit: Option<Balance> = None;
/// Release delay - None disables permissionless release
pub const ReleaseDelayNone: Option<BlockNumber> = None;
}
/// Calls that cannot be paused by the tx-pause pallet.
pub struct TxPauseWhitelistedCalls<R>(PhantomData<R>);
/// Whitelist `Balances::transfer_keep_alive`, all others are pauseable.
impl<R> Contains<RuntimeCallNameOf<R>> for TxPauseWhitelistedCalls<R>
where
R: pallet_tx_pause::Config,
{
fn contains(full_name: &RuntimeCallNameOf<R>) -> bool {
match (full_name.0.as_slice(), full_name.1.as_slice()) {
// sudo calls
(b"Sudo", _) => true,
// SafeMode calls
(b"SafeMode", _) => true,
_ => false,
}
}
}
/// Combined Call Filter that applies Normal, SafeMode, and TxPause filters
/// This filter is generic over the runtime call type and identical across all runtimes
pub struct RuntimeCallFilter<Call, NormalFilter, SafeModeFilter, TxPauseFilter>(
PhantomData<(Call, NormalFilter, SafeModeFilter, TxPauseFilter)>,
);
impl<Call, NormalFilter, SafeModeFilter, TxPauseFilter> Contains<Call>
for RuntimeCallFilter<Call, NormalFilter, SafeModeFilter, TxPauseFilter>
where
NormalFilter: Contains<Call>,
SafeModeFilter: Contains<Call>,
TxPauseFilter: Contains<Call>,
{
fn contains(call: &Call) -> bool {
NormalFilter::contains(call)
&& SafeModeFilter::contains(call)
&& TxPauseFilter::contains(call)
}
}

View file

@ -64,6 +64,8 @@ pallet-outbound-commitment-store = { workspace = true }
pallet-datahaven-native-transfer = { workspace = true }
pallet-parameters = { workspace = true }
pallet-preimage = { workspace = true }
pallet-safe-mode = { workspace = true }
pallet-tx-pause = { workspace = true }
pallet-proxy = { workspace = true }
pallet-referenda = { workspace = true }
pallet-scheduler = { workspace = true }
@ -211,6 +213,8 @@ std = [
"pallet-offences/std",
"pallet-parameters/std",
"pallet-preimage/std",
"pallet-safe-mode/std",
"pallet-tx-pause/std",
"pallet-referenda/std",
"pallet-proxy/std",
"pallet-scheduler/std",
@ -310,6 +314,8 @@ runtime-benchmarks = [
"pallet-offences/runtime-benchmarks",
"pallet-parameters/runtime-benchmarks",
"pallet-preimage/runtime-benchmarks",
"pallet-safe-mode/runtime-benchmarks",
"pallet-tx-pause/runtime-benchmarks",
"pallet-referenda/runtime-benchmarks",
"pallet-proxy/runtime-benchmarks",
"pallet-scheduler/runtime-benchmarks",
@ -358,6 +364,8 @@ try-runtime = [
"pallet-offences/try-runtime",
"pallet-parameters/try-runtime",
"pallet-preimage/try-runtime",
"pallet-safe-mode/try-runtime",
"pallet-tx-pause/try-runtime",
"pallet-referenda/try-runtime",
"pallet-proxy/try-runtime",
"pallet-scheduler/try-runtime",

View file

@ -53,6 +53,8 @@ frame_benchmarking::define_benchmarks!(
[pallet_transaction_payment, TransactionPayment]
[pallet_parameters, Parameters]
[pallet_message_queue, MessageQueue]
[pallet_safe_mode, SafeMode]
[pallet_tx_pause, TxPause]
// EVM pallets
[pallet_evm, Evm]

View file

@ -35,8 +35,8 @@ use super::{
ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations,
Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda,
Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin,
RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
};
use codec::{Decode, Encode, MaxEncodedLen};
@ -91,6 +91,10 @@ use datahaven_runtime_common::{
FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen,
MigrationIdentifierMaxLen, MigrationStatusHandler,
},
safe_mode::{
ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit,
SafeModeExtendDeposit, TxPauseWhitelistedCalls,
},
time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK},
};
use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor};
@ -233,6 +237,36 @@ impl Contains<RuntimeCall> for NormalCallFilter {
}
}
/// Calls that can bypass the safe-mode pallet.
/// These calls are essential for emergency governance and system maintenance.
pub struct SafeModeWhitelistedCalls;
impl Contains<RuntimeCall> for SafeModeWhitelistedCalls {
fn contains(call: &RuntimeCall) -> bool {
match call {
// Core system calls
RuntimeCall::System(_) => true,
// Safe mode management
RuntimeCall::SafeMode(_) => true,
// Transaction pause management
RuntimeCall::TxPause(_) => true,
// Emergency admin access (testnet/dev only)
RuntimeCall::Sudo(_) => true,
// Governance infrastructure - critical for emergency responses
RuntimeCall::Whitelist(_) => true,
RuntimeCall::Preimage(_) => true,
RuntimeCall::Scheduler(_) => true,
RuntimeCall::ConvictionVoting(_) => true,
RuntimeCall::Referenda(_) => true,
RuntimeCall::TechnicalCommittee(_) => true,
RuntimeCall::TreasuryCouncil(_) => true,
_ => false,
}
}
}
pub type MainnetRuntimeCallFilter =
RuntimeCallFilter<RuntimeCall, NormalCallFilter, SafeMode, TxPause>;
/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from
/// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`),
/// but overridden as needed.
@ -265,8 +299,8 @@ impl frame_system::Config for Runtime {
type MaxConsumers = frame_support::traits::ConstU32<16>;
type SystemWeightInfo = mainnet_weights::frame_system::WeightInfo<Runtime>;
type MultiBlockMigrator = MultiBlockMigrations;
/// Use the NormalCallFilter to restrict certain runtime calls
type BaseCallFilter = NormalCallFilter;
/// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions
type BaseCallFilter = MainnetRuntimeCallFilter;
}
// 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks.
@ -1480,6 +1514,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime {
type WeightInfo = mainnet_weights::pallet_datahaven_native_transfer::WeightInfo<Runtime>;
}
//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
//║ SAFE MODE & TX PAUSE PALLETS ║
//╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
impl pallet_safe_mode::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type RuntimeHoldReason = RuntimeHoldReason;
type WhitelistedCalls = SafeModeWhitelistedCalls;
type EnterDuration = SafeModeDuration;
type ExtendDuration = SafeModeDuration;
type EnterDepositAmount = SafeModeEnterDeposit;
type ExtendDepositAmount = SafeModeExtendDeposit;
type ForceEnterOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
type ForceExtendOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
type ForceExitOrigin = EnsureRoot<AccountId>;
type ForceDepositOrigin = EnsureRoot<AccountId>;
type ReleaseDelay = ReleaseDelayNone;
type Notify = ();
type WeightInfo = mainnet_weights::pallet_safe_mode::WeightInfo<Runtime>;
}
impl pallet_tx_pause::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type RuntimeCall = RuntimeCall;
type PauseOrigin = EnsureRoot<AccountId>;
type UnpauseOrigin = EnsureRoot<AccountId>;
type WhitelistedCalls = TxPauseWhitelistedCalls<Runtime>;
type MaxNameLen = ConstU32<256>;
type WeightInfo = mainnet_weights::pallet_tx_pause::WeightInfo<Runtime>;
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -378,6 +378,12 @@ mod runtime {
#[runtime::pallet_index(39)]
pub type MultiBlockMigrations = pallet_migrations;
#[runtime::pallet_index(103)]
pub type SafeMode = pallet_safe_mode;
#[runtime::pallet_index(104)]
pub type TxPause = pallet_tx_pause;
// ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝
// ╔═════════════════════════ Governance Pallets ════════════════════╗

View file

@ -41,11 +41,13 @@ pub mod pallet_multisig;
pub mod pallet_parameters;
pub mod pallet_preimage;
pub mod pallet_proxy;
pub mod pallet_safe_mode;
pub mod pallet_scheduler;
pub mod pallet_sudo;
pub mod pallet_timestamp;
pub mod pallet_transaction_payment;
pub mod pallet_treasury;
pub mod pallet_tx_pause;
pub mod pallet_utility;
// Governance pallets

View file

@ -0,0 +1,7 @@
// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks.
//
// We reuse the upstream Substrate weight assumptions which are conservative enough for
// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate
// weights in this module via the runtime benchmarking CLI.
pub type WeightInfo<T> = pallet_safe_mode::weights::SubstrateWeight<T>;

View file

@ -0,0 +1,7 @@
// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks.
//
// We reuse the upstream Substrate weight assumptions which are conservative enough for
// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in
// this module via the runtime benchmarking CLI.
pub type WeightInfo<T> = pallet_tx_pause::weights::SubstrateWeight<T>;

View file

@ -5,6 +5,7 @@ pub mod governance;
mod migrations;
mod native_token_transfer;
mod proxy;
mod safe_mode_tx_pause;
use common::*;
use datahaven_mainnet_runtime::{

View file

@ -0,0 +1,431 @@
#![allow(clippy::too_many_arguments)]
#[path = "common.rs"]
mod common;
use common::{account_id, ExtBuilder, ALICE, BOB};
use datahaven_mainnet_runtime::{
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, System, UncheckedExtrinsic,
};
use frame_support::{assert_noop, assert_ok, BoundedVec};
use pallet_safe_mode::EnteredUntil;
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
use sp_runtime::{
traits::Dispatchable,
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
};
use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3;
fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf<Runtime> {
use frame_support::traits::GetCallMetadata;
let metadata = call.get_call_metadata();
(
BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(),
BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(),
)
}
fn transfer_call(amount: u128) -> RuntimeCall {
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
dest: account_id(BOB),
value: amount,
})
}
mod safe_mode {
use super::*;
#[test]
fn force_enter_requires_root() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
assert_noop!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
sp_runtime::DispatchError::BadOrigin
);
assert!(EnteredUntil::<Runtime>::get().is_some());
System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::<
Runtime,
>::Entered {
until: EnteredUntil::<Runtime>::get().unwrap(),
}));
});
}
#[test]
fn active_safe_mode_blocks_non_whitelisted_calls() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let xt = transfer_call(1u128);
let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into());
let validity = Runtime::validate_transaction(
TransactionSource::External,
unchecked_xt,
Default::default(),
);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
});
}
#[test]
fn whitelisted_calls_dispatch_in_safe_mode() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
assert!(EnteredUntil::<Runtime>::get().is_none());
});
}
}
mod tx_pause {
use super::*;
#[test]
fn pause_requires_root() {
ExtBuilder::default().build().execute_with(|| {
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
assert_noop!(
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
sp_runtime::DispatchError::BadOrigin
);
assert_ok!(
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
.dispatch(RuntimeOrigin::root())
);
});
}
#[test]
fn paused_call_is_blocked() {
ExtBuilder::default().build().execute_with(|| {
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
assert_eq!(
Runtime::validate_transaction(TransactionSource::External, xt, Default::default()),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_noop!(
call.clone()
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
assert_ok!(
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
.dispatch(RuntimeOrigin::root())
);
// After unpause, the call should be dispatchable
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
});
}
#[test]
fn whitelisted_call_cannot_be_paused() {
ExtBuilder::default().build().execute_with(|| {
let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
let call_name = call_name(&call);
assert_noop!(
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()),
TxPauseError::<Runtime>::Unpausable
);
});
}
}
mod combined_behaviour {
use super::*;
#[test]
fn dual_restrictions_require_both_to_clear() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let validity = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
ident: call_name.clone()
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let still_blocked = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
assert_eq!(
still_blocked,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
// After exiting safe mode and unpausing, call should be dispatchable
assert_ok!(call
.clone()
.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.into());
assert_eq!(
Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default()
),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
});
}
#[test]
fn control_plane_calls_work_under_restrictions() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
ident: call_name.clone()
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
});
}
#[test]
fn governance_whitelisted_calls_work_during_safe_mode() {
use sp_core::H256;
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.with_balances(vec![(account_id(ALICE), 1_000_000_000_000)])
.build()
.execute_with(|| {
// Enter safe mode
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
// Verify safe mode is active
assert!(EnteredUntil::<Runtime>::get().is_some());
// Verify normal calls are blocked during safe mode
let normal_call = transfer_call(100);
assert_noop!(
normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
// Test Whitelist pallet - critical for emergency runtime upgrades
let call_hash = H256::random();
assert_ok!(
RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash })
.dispatch(RuntimeOrigin::root())
);
// Test Preimage pallet - required for storing governance call data
let dummy_preimage = vec![1u8; 32];
let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage {
bytes: dummy_preimage,
})
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
match preimage_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Preimage calls should not be filtered by safe mode"
);
}
}
// Test Scheduler pallet - needed for time-delayed governance actions
let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel {
when: 100,
index: 0,
})
.dispatch(RuntimeOrigin::root());
match scheduler_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Scheduler calls should not be filtered by safe mode"
);
}
}
// Test Referenda pallet - core OpenGov proposal system
let referenda_result =
RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 })
.dispatch(RuntimeOrigin::root());
match referenda_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Referenda calls should not be filtered by safe mode"
);
}
}
// Test ConvictionVoting - allows token holders to vote during emergencies
let voting_result = RuntimeCall::ConvictionVoting(
pallet_conviction_voting::Call::remove_other_vote {
target: account_id(BOB),
class: 0,
index: 0,
},
)
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
match voting_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"ConvictionVoting calls should not be filtered by safe mode"
);
}
}
// Test TechnicalCommittee - expert oversight for emergency actions
let tech_committee_result =
RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members {
new_members: vec![account_id(ALICE)],
prime: None,
old_count: 0,
})
.dispatch(RuntimeOrigin::root());
match tech_committee_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"TechnicalCommittee calls should not be filtered by safe mode"
);
}
}
});
}
}

View file

@ -64,6 +64,8 @@ pallet-outbound-commitment-store = { workspace = true }
pallet-datahaven-native-transfer = { workspace = true }
pallet-parameters = { workspace = true }
pallet-preimage = { workspace = true }
pallet-safe-mode = { workspace = true }
pallet-tx-pause = { workspace = true }
pallet-proxy = { workspace = true }
pallet-referenda = { workspace = true }
pallet-scheduler = { workspace = true }
@ -212,6 +214,8 @@ std = [
"pallet-offences/std",
"pallet-parameters/std",
"pallet-preimage/std",
"pallet-safe-mode/std",
"pallet-tx-pause/std",
"pallet-referenda/std",
"pallet-proxy/std",
"pallet-scheduler/std",
@ -311,6 +315,8 @@ runtime-benchmarks = [
"pallet-offences/runtime-benchmarks",
"pallet-parameters/runtime-benchmarks",
"pallet-preimage/runtime-benchmarks",
"pallet-safe-mode/runtime-benchmarks",
"pallet-tx-pause/runtime-benchmarks",
"pallet-referenda/runtime-benchmarks",
"pallet-proxy/runtime-benchmarks",
"pallet-scheduler/runtime-benchmarks",
@ -359,6 +365,8 @@ try-runtime = [
"pallet-offences/try-runtime",
"pallet-parameters/try-runtime",
"pallet-preimage/try-runtime",
"pallet-safe-mode/try-runtime",
"pallet-tx-pause/try-runtime",
"pallet-referenda/try-runtime",
"pallet-proxy/try-runtime",
"pallet-scheduler/try-runtime",

View file

@ -53,6 +53,8 @@ frame_benchmarking::define_benchmarks!(
[pallet_transaction_payment, TransactionPayment]
[pallet_parameters, Parameters]
[pallet_message_queue, MessageQueue]
[pallet_safe_mode, SafeMode]
[pallet_tx_pause, TxPause]
// Governance pallets
[pallet_collective_technical_committee, TechnicalCommittee]

View file

@ -35,8 +35,8 @@ use super::{
ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations,
Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda,
Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin,
RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
};
use codec::{Decode, Encode, MaxEncodedLen};
@ -91,6 +91,10 @@ use datahaven_runtime_common::{
FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen,
MigrationIdentifierMaxLen, MigrationStatusHandler,
},
safe_mode::{
ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit,
SafeModeExtendDeposit, TxPauseWhitelistedCalls,
},
time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK},
};
use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor};
@ -233,6 +237,36 @@ impl Contains<RuntimeCall> for NormalCallFilter {
}
}
/// Calls that can bypass the safe-mode pallet.
/// These calls are essential for emergency governance and system maintenance.
pub struct SafeModeWhitelistedCalls;
impl Contains<RuntimeCall> for SafeModeWhitelistedCalls {
fn contains(call: &RuntimeCall) -> bool {
match call {
// Core system calls
RuntimeCall::System(_) => true,
// Safe mode management
RuntimeCall::SafeMode(_) => true,
// Transaction pause management
RuntimeCall::TxPause(_) => true,
// Emergency admin access (testnet/dev only)
RuntimeCall::Sudo(_) => true,
// Governance infrastructure - critical for emergency responses
RuntimeCall::Whitelist(_) => true,
RuntimeCall::Preimage(_) => true,
RuntimeCall::Scheduler(_) => true,
RuntimeCall::ConvictionVoting(_) => true,
RuntimeCall::Referenda(_) => true,
RuntimeCall::TechnicalCommittee(_) => true,
RuntimeCall::TreasuryCouncil(_) => true,
_ => false,
}
}
}
pub type StagenetRuntimeCallFilter =
RuntimeCallFilter<RuntimeCall, NormalCallFilter, SafeMode, TxPause>;
/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from
/// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`),
/// but overridden as needed.
@ -265,8 +299,8 @@ impl frame_system::Config for Runtime {
type MaxConsumers = frame_support::traits::ConstU32<16>;
type SystemWeightInfo = stagenet_weights::frame_system::WeightInfo<Runtime>;
type MultiBlockMigrator = MultiBlockMigrations;
/// Use the NormalCallFilter to restrict certain runtime calls
type BaseCallFilter = NormalCallFilter;
/// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions
type BaseCallFilter = StagenetRuntimeCallFilter;
}
// 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks.
@ -1481,6 +1515,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime {
type WeightInfo = stagenet_weights::pallet_datahaven_native_transfer::WeightInfo<Runtime>;
}
//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
//║ SAFE MODE & TX PAUSE PALLETS ║
//╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
impl pallet_safe_mode::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type RuntimeHoldReason = RuntimeHoldReason;
type WhitelistedCalls = SafeModeWhitelistedCalls;
type EnterDuration = SafeModeDuration;
type ExtendDuration = SafeModeDuration;
type EnterDepositAmount = SafeModeEnterDeposit;
type ExtendDepositAmount = SafeModeExtendDeposit;
type ForceEnterOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
type ForceExtendOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
type ForceExitOrigin = EnsureRoot<AccountId>;
type ForceDepositOrigin = EnsureRoot<AccountId>;
type ReleaseDelay = ReleaseDelayNone;
type Notify = ();
type WeightInfo = stagenet_weights::pallet_safe_mode::WeightInfo<Runtime>;
}
impl pallet_tx_pause::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type RuntimeCall = RuntimeCall;
type PauseOrigin = EnsureRoot<AccountId>;
type UnpauseOrigin = EnsureRoot<AccountId>;
type WhitelistedCalls = TxPauseWhitelistedCalls<Runtime>;
type MaxNameLen = ConstU32<256>;
type WeightInfo = stagenet_weights::pallet_tx_pause::WeightInfo<Runtime>;
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -380,6 +380,12 @@ mod runtime {
#[runtime::pallet_index(39)]
pub type MultiBlockMigrations = pallet_migrations;
#[runtime::pallet_index(103)]
pub type SafeMode = pallet_safe_mode;
#[runtime::pallet_index(104)]
pub type TxPause = pallet_tx_pause;
// ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝
// ╔═════════════════════════ Governance Pallets ════════════════════╗

View file

@ -43,11 +43,13 @@ pub mod pallet_multisig;
pub mod pallet_parameters;
pub mod pallet_preimage;
pub mod pallet_proxy;
pub mod pallet_safe_mode;
pub mod pallet_scheduler;
pub mod pallet_sudo;
pub mod pallet_timestamp;
pub mod pallet_transaction_payment;
pub mod pallet_treasury;
pub mod pallet_tx_pause;
pub mod pallet_utility;
// Governance pallets

View file

@ -0,0 +1,7 @@
// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks.
//
// We reuse the upstream Substrate weight assumptions which are conservative enough for
// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate
// weights in this module via the runtime benchmarking CLI.
pub type WeightInfo<T> = pallet_safe_mode::weights::SubstrateWeight<T>;

View file

@ -0,0 +1,7 @@
// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks.
//
// We reuse the upstream Substrate weight assumptions which are conservative enough for
// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in
// this module via the runtime benchmarking CLI.
pub type WeightInfo<T> = pallet_tx_pause::weights::SubstrateWeight<T>;

View file

@ -4,6 +4,7 @@ pub mod common;
pub mod governance;
mod native_token_transfer;
mod proxy;
mod safe_mode_tx_pause;
use common::*;
use datahaven_stagenet_runtime::{

View file

@ -0,0 +1,432 @@
#![allow(clippy::too_many_arguments)]
#[path = "common.rs"]
mod common;
use common::{account_id, ExtBuilder, ALICE, BOB};
use datahaven_stagenet_runtime::{
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System, TxPause,
UncheckedExtrinsic,
};
use frame_support::{assert_noop, assert_ok, BoundedVec};
use pallet_safe_mode::EnteredUntil;
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
use sp_runtime::{
traits::Dispatchable,
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
};
use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3;
fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf<Runtime> {
use frame_support::traits::GetCallMetadata;
let metadata = call.get_call_metadata();
(
BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(),
BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(),
)
}
fn transfer_call(amount: u128) -> RuntimeCall {
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
dest: account_id(BOB),
value: amount,
})
}
mod safe_mode {
use super::*;
#[test]
fn force_enter_requires_root() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
assert_noop!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
sp_runtime::DispatchError::BadOrigin
);
assert!(EnteredUntil::<Runtime>::get().is_some());
System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::<
Runtime,
>::Entered {
until: EnteredUntil::<Runtime>::get().unwrap(),
}));
});
}
#[test]
fn active_safe_mode_blocks_non_whitelisted_calls() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let xt = transfer_call(1u128);
let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into());
let validity = Runtime::validate_transaction(
TransactionSource::External,
unchecked_xt,
Default::default(),
);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
});
}
#[test]
fn whitelisted_calls_dispatch_in_safe_mode() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
assert!(EnteredUntil::<Runtime>::get().is_none());
});
}
}
mod tx_pause {
use super::*;
#[test]
fn pause_requires_root() {
ExtBuilder::default().build().execute_with(|| {
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
assert_noop!(
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
sp_runtime::DispatchError::BadOrigin
);
assert_ok!(
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
.dispatch(RuntimeOrigin::root())
);
});
}
#[test]
fn paused_call_is_blocked() {
ExtBuilder::default().build().execute_with(|| {
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
assert_eq!(
Runtime::validate_transaction(TransactionSource::External, xt, Default::default()),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_noop!(
call.clone()
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
assert_ok!(
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
.dispatch(RuntimeOrigin::root())
);
// After unpause, the call should be dispatchable
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
});
}
#[test]
fn whitelisted_call_cannot_be_paused() {
ExtBuilder::default().build().execute_with(|| {
let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
let call_name = call_name(&call);
assert_noop!(
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()),
TxPauseError::<Runtime>::Unpausable
);
});
}
}
mod combined_behaviour {
use super::*;
#[test]
fn dual_restrictions_require_both_to_clear() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let validity = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
ident: call_name.clone()
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let still_blocked = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
assert_eq!(
still_blocked,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
// After exiting safe mode and unpausing, call should be dispatchable
assert_ok!(call
.clone()
.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.into());
assert_eq!(
Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default()
),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
});
}
#[test]
fn control_plane_calls_work_under_restrictions() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
ident: call_name.clone()
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
});
}
#[test]
fn governance_whitelisted_calls_work_during_safe_mode() {
use sp_core::H256;
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.with_balances(vec![(account_id(ALICE), 1_000_000_000_000)])
.build()
.execute_with(|| {
// Enter safe mode
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
// Verify safe mode is active
assert!(EnteredUntil::<Runtime>::get().is_some());
// Verify normal calls are blocked during safe mode
let normal_call = transfer_call(100);
assert_noop!(
normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
// Test Whitelist pallet - critical for emergency runtime upgrades
let call_hash = H256::random();
assert_ok!(
RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash })
.dispatch(RuntimeOrigin::root())
);
// Test Preimage pallet - required for storing governance call data
let dummy_preimage = vec![1u8; 32];
let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage {
bytes: dummy_preimage,
})
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
match preimage_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Preimage calls should not be filtered by safe mode"
);
}
}
// Test Scheduler pallet - needed for time-delayed governance actions
let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel {
when: 100,
index: 0,
})
.dispatch(RuntimeOrigin::root());
match scheduler_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Scheduler calls should not be filtered by safe mode"
);
}
}
// Test Referenda pallet - core OpenGov proposal system
let referenda_result =
RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 })
.dispatch(RuntimeOrigin::root());
match referenda_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Referenda calls should not be filtered by safe mode"
);
}
}
// Test ConvictionVoting - allows token holders to vote during emergencies
let voting_result = RuntimeCall::ConvictionVoting(
pallet_conviction_voting::Call::remove_other_vote {
target: account_id(BOB),
class: 0,
index: 0,
},
)
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
match voting_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"ConvictionVoting calls should not be filtered by safe mode"
);
}
}
// Test TechnicalCommittee - expert oversight for emergency actions
let tech_committee_result =
RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members {
new_members: vec![account_id(ALICE)],
prime: None,
old_count: 0,
})
.dispatch(RuntimeOrigin::root());
match tech_committee_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"TechnicalCommittee calls should not be filtered by safe mode"
);
}
}
});
}
}

View file

@ -64,6 +64,8 @@ pallet-offences = { workspace = true }
pallet-outbound-commitment-store = { workspace = true }
pallet-parameters = { workspace = true }
pallet-preimage = { workspace = true }
pallet-safe-mode = { workspace = true }
pallet-tx-pause = { workspace = true }
pallet-proxy = { workspace = true }
pallet-referenda = { workspace = true }
pallet-scheduler = { workspace = true }
@ -209,6 +211,8 @@ std = [
"pallet-offences/std",
"pallet-parameters/std",
"pallet-preimage/std",
"pallet-safe-mode/std",
"pallet-tx-pause/std",
"pallet-referenda/std",
"pallet-proxy/std",
"pallet-scheduler/std",
@ -310,6 +314,8 @@ runtime-benchmarks = [
"pallet-offences/runtime-benchmarks",
"pallet-parameters/runtime-benchmarks",
"pallet-preimage/runtime-benchmarks",
"pallet-safe-mode/runtime-benchmarks",
"pallet-tx-pause/runtime-benchmarks",
"pallet-referenda/runtime-benchmarks",
"pallet-proxy/runtime-benchmarks",
"pallet-scheduler/runtime-benchmarks",
@ -358,6 +364,8 @@ try-runtime = [
"pallet-offences/try-runtime",
"pallet-parameters/try-runtime",
"pallet-preimage/try-runtime",
"pallet-safe-mode/try-runtime",
"pallet-tx-pause/try-runtime",
"pallet-referenda/try-runtime",
"pallet-proxy/try-runtime",
"pallet-scheduler/try-runtime",

View file

@ -52,6 +52,8 @@ frame_benchmarking::define_benchmarks!(
[pallet_transaction_payment, TransactionPayment]
[pallet_parameters, Parameters]
[pallet_message_queue, MessageQueue]
[pallet_safe_mode, SafeMode]
[pallet_tx_pause, TxPause]
// EVM pallets
[pallet_evm, Evm]

View file

@ -35,8 +35,8 @@ use super::{
ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, MultiBlockMigrations,
Nonce, Offences, OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Referenda,
Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin,
RuntimeTask, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
RuntimeTask, SafeMode, Scheduler, Session, SessionKeys, Signature, System, Timestamp, Treasury,
TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT, MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT,
NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
};
use codec::{Decode, Encode, MaxEncodedLen};
@ -91,6 +91,10 @@ use datahaven_runtime_common::{
FailedMigrationHandler as DefaultFailedMigrationHandler, MigrationCursorMaxLen,
MigrationIdentifierMaxLen, MigrationStatusHandler,
},
safe_mode::{
ReleaseDelayNone, RuntimeCallFilter, SafeModeDuration, SafeModeEnterDeposit,
SafeModeExtendDeposit, TxPauseWhitelistedCalls,
},
time::{EpochDurationInBlocks, DAYS, MILLISECS_PER_BLOCK},
};
use dhp_bridge::{EigenLayerMessageProcessor, NativeTokenTransferMessageProcessor};
@ -233,6 +237,36 @@ impl Contains<RuntimeCall> for NormalCallFilter {
}
}
/// Calls that can bypass the safe-mode pallet.
/// These calls are essential for emergency governance and system maintenance.
pub struct SafeModeWhitelistedCalls;
impl Contains<RuntimeCall> for SafeModeWhitelistedCalls {
fn contains(call: &RuntimeCall) -> bool {
match call {
// Core system calls
RuntimeCall::System(_) => true,
// Safe mode management
RuntimeCall::SafeMode(_) => true,
// Transaction pause management
RuntimeCall::TxPause(_) => true,
// Emergency admin access (testnet/dev only)
RuntimeCall::Sudo(_) => true,
// Governance infrastructure - critical for emergency responses
RuntimeCall::Whitelist(_) => true,
RuntimeCall::Preimage(_) => true,
RuntimeCall::Scheduler(_) => true,
RuntimeCall::ConvictionVoting(_) => true,
RuntimeCall::Referenda(_) => true,
RuntimeCall::TechnicalCommittee(_) => true,
RuntimeCall::TreasuryCouncil(_) => true,
_ => false,
}
}
}
pub type TestnetRuntimeCallFilter =
RuntimeCallFilter<RuntimeCall, NormalCallFilter, SafeMode, TxPause>;
/// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from
/// [`SoloChainDefaultConfig`](`struct@frame_system::config_preludes::SolochainDefaultConfig`),
/// but overridden as needed.
@ -265,8 +299,8 @@ impl frame_system::Config for Runtime {
type MaxConsumers = frame_support::traits::ConstU32<16>;
type SystemWeightInfo = testnet_weights::frame_system::WeightInfo<Runtime>;
type MultiBlockMigrator = MultiBlockMigrations;
/// Use the NormalCallFilter to restrict certain runtime calls
type BaseCallFilter = NormalCallFilter;
/// Use the combined call filter to apply Normal, SafeMode, and TxPause restrictions
type BaseCallFilter = TestnetRuntimeCallFilter;
}
// 1 in 4 blocks (on average, not counting collisions) will be primary babe blocks.
@ -1479,6 +1513,38 @@ impl pallet_datahaven_native_transfer::Config for Runtime {
type WeightInfo = testnet_weights::pallet_datahaven_native_transfer::WeightInfo<Runtime>;
}
//╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
//║ SAFE MODE & TX PAUSE PALLETS ║
//╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
impl pallet_safe_mode::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type RuntimeHoldReason = RuntimeHoldReason;
type WhitelistedCalls = SafeModeWhitelistedCalls;
type EnterDuration = SafeModeDuration;
type ExtendDuration = SafeModeDuration;
type EnterDepositAmount = SafeModeEnterDeposit;
type ExtendDepositAmount = SafeModeExtendDeposit;
type ForceEnterOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
type ForceExtendOrigin = EnsureRootWithSuccess<AccountId, SafeModeDuration>;
type ForceExitOrigin = EnsureRoot<AccountId>;
type ForceDepositOrigin = EnsureRoot<AccountId>;
type ReleaseDelay = ReleaseDelayNone;
type Notify = ();
type WeightInfo = testnet_weights::pallet_safe_mode::WeightInfo<Runtime>;
}
impl pallet_tx_pause::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type RuntimeCall = RuntimeCall;
type PauseOrigin = EnsureRoot<AccountId>;
type UnpauseOrigin = EnsureRoot<AccountId>;
type WhitelistedCalls = TxPauseWhitelistedCalls<Runtime>;
type MaxNameLen = ConstU32<256>;
type WeightInfo = testnet_weights::pallet_tx_pause::WeightInfo<Runtime>;
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -377,6 +377,12 @@ mod runtime {
#[runtime::pallet_index(39)]
pub type MultiBlockMigrations = pallet_migrations;
#[runtime::pallet_index(103)]
pub type SafeMode = pallet_safe_mode;
#[runtime::pallet_index(104)]
pub type TxPause = pallet_tx_pause;
// ╚═════════════════ Polkadot SDK Utility Pallets ══════════════════╝
// ╔═════════════════════════ Governance Pallets ════════════════════╗

View file

@ -43,11 +43,13 @@ pub mod pallet_multisig;
pub mod pallet_parameters;
pub mod pallet_preimage;
pub mod pallet_proxy;
pub mod pallet_safe_mode;
pub mod pallet_scheduler;
pub mod pallet_sudo;
pub mod pallet_timestamp;
pub mod pallet_transaction_payment;
pub mod pallet_treasury;
pub mod pallet_tx_pause;
pub mod pallet_utility;
// Governance pallets

View file

@ -0,0 +1,7 @@
// Placeholder weight mapping for `pallet-safe-mode` until we record chain-specific benchmarks.
//
// We reuse the upstream Substrate weight assumptions which are conservative enough for
// bootstrapping. Once DataHaven-specific safe-mode migrations are added we should regenerate
// weights in this module via the runtime benchmarking CLI.
pub type WeightInfo<T> = pallet_safe_mode::weights::SubstrateWeight<T>;

View file

@ -0,0 +1,7 @@
// Placeholder weight mapping for `pallet-tx-pause` until we record chain-specific benchmarks.
//
// We reuse the upstream Substrate weight assumptions which are conservative enough for
// bootstrapping. Once DataHaven-specific paused-call logic is added we should regenerate weights in
// this module via the runtime benchmarking CLI.
pub type WeightInfo<T> = pallet_tx_pause::weights::SubstrateWeight<T>;

View file

@ -4,6 +4,7 @@ pub mod common;
pub mod governance;
mod native_token_transfer;
mod proxy;
mod safe_mode_tx_pause;
use common::*;
use datahaven_testnet_runtime::{

View file

@ -0,0 +1,535 @@
#![allow(clippy::too_many_arguments)]
#[path = "common.rs"]
mod common;
use common::{account_id, ExtBuilder, ALICE, BOB};
use datahaven_testnet_runtime::{
Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SafeMode, System, TxPause,
UncheckedExtrinsic,
};
use frame_support::{assert_noop, assert_ok, BoundedVec};
use pallet_safe_mode::EnteredUntil;
use pallet_tx_pause::{Error as TxPauseError, RuntimeCallNameOf};
use sp_runtime::{
traits::Dispatchable,
transaction_validity::{InvalidTransaction, TransactionSource, TransactionValidityError},
};
use sp_transaction_pool::runtime_api::runtime_decl_for_tagged_transaction_queue::TaggedTransactionQueueV3;
fn call_name(call: &RuntimeCall) -> RuntimeCallNameOf<Runtime> {
use frame_support::traits::GetCallMetadata;
let metadata = call.get_call_metadata();
(
BoundedVec::try_from(metadata.pallet_name.as_bytes().to_vec()).unwrap(),
BoundedVec::try_from(metadata.function_name.as_bytes().to_vec()).unwrap(),
)
}
fn transfer_call(amount: u128) -> RuntimeCall {
RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive {
dest: account_id(BOB),
value: amount,
})
}
mod safe_mode {
use super::*;
#[test]
fn force_enter_requires_root() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
assert_noop!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
sp_runtime::DispatchError::BadOrigin
);
assert!(EnteredUntil::<Runtime>::get().is_some());
System::assert_last_event(RuntimeEvent::SafeMode(pallet_safe_mode::Event::<
Runtime,
>::Entered {
until: EnteredUntil::<Runtime>::get().unwrap(),
}));
});
}
#[test]
fn active_safe_mode_blocks_non_whitelisted_calls() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let xt = transfer_call(1u128);
let unchecked_xt = UncheckedExtrinsic::new_bare(xt.into());
let validity = Runtime::validate_transaction(
TransactionSource::External,
unchecked_xt,
Default::default(),
);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
});
}
#[test]
fn whitelisted_calls_dispatch_in_safe_mode() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
assert!(EnteredUntil::<Runtime>::get().is_none());
});
}
#[test]
fn exit_restores_normal_flow() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.with_balances(vec![(account_id(ALICE), 1_000_000)])
.build()
.execute_with(|| {
// Enter safe mode
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(100);
// Verify call is blocked in safe mode
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
assert_eq!(
Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default()
),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
// Exit safe mode
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
// Verify call now works
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
});
}
#[test]
fn sudo_bypass() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.with_balances(vec![(account_id(ALICE), 1_000_000)])
.build()
.execute_with(|| {
// Enter safe mode
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let transfer = transfer_call(100);
// Wrap in sudo call
let sudo_call = RuntimeCall::Sudo(pallet_sudo::Call::sudo {
call: Box::new(transfer),
});
// Sudo should bypass safe mode filter
assert_ok!(sudo_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
});
}
}
mod tx_pause {
use super::*;
#[test]
fn pause_requires_root() {
ExtBuilder::default().build().execute_with(|| {
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
assert_noop!(
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
sp_runtime::DispatchError::BadOrigin
);
assert_ok!(
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
.dispatch(RuntimeOrigin::root())
);
});
}
#[test]
fn paused_call_is_blocked() {
ExtBuilder::default().build().execute_with(|| {
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
assert_eq!(
Runtime::validate_transaction(TransactionSource::External, xt, Default::default()),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_noop!(
call.clone()
.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
assert_ok!(
RuntimeCall::TxPause(pallet_tx_pause::Call::unpause { ident: call_name })
.dispatch(RuntimeOrigin::root())
);
// After unpause, the call should be dispatchable
assert_ok!(call.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
});
}
#[test]
fn whitelisted_call_cannot_be_paused() {
ExtBuilder::default().build().execute_with(|| {
let call = RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {});
let call_name = call_name(&call);
assert_noop!(
RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()),
TxPauseError::<Runtime>::Unpausable
);
});
}
}
mod combined_behaviour {
use super::*;
#[test]
fn dual_restrictions_require_both_to_clear() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let validity = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
assert_eq!(
validity,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
ident: call_name.clone()
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let still_blocked = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
assert_eq!(
still_blocked,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
// After exiting safe mode and unpausing, call should be dispatchable
assert_ok!(call
.clone()
.dispatch(RuntimeOrigin::signed(account_id(ALICE))));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
let xt = UncheckedExtrinsic::new_bare(call.into());
assert_eq!(
Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default()
),
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
});
}
#[test]
fn control_plane_calls_work_under_restrictions() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.build()
.execute_with(|| {
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(1u128);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name.clone(),
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::unpause {
ident: call_name.clone()
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
assert_ok!(RuntimeCall::SafeMode(pallet_safe_mode::Call::force_exit {})
.dispatch(RuntimeOrigin::root()));
});
}
#[test]
fn governance_whitelisted_calls_work_during_safe_mode() {
use sp_core::H256;
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.with_balances(vec![(account_id(ALICE), 1_000_000_000_000)])
.build()
.execute_with(|| {
// Enter safe mode
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
// Verify safe mode is active
assert!(EnteredUntil::<Runtime>::get().is_some());
// Verify normal calls are blocked during safe mode
let normal_call = transfer_call(100);
assert_noop!(
normal_call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
// Test Whitelist pallet - critical for emergency runtime upgrades
let call_hash = H256::random();
assert_ok!(
RuntimeCall::Whitelist(pallet_whitelist::Call::whitelist_call { call_hash })
.dispatch(RuntimeOrigin::root())
);
// Test Preimage pallet - required for storing governance call data
let dummy_preimage = vec![1u8; 32];
let preimage_result = RuntimeCall::Preimage(pallet_preimage::Call::note_preimage {
bytes: dummy_preimage,
})
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
match preimage_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Preimage calls should not be filtered by safe mode"
);
}
}
// Test Scheduler pallet - needed for time-delayed governance actions
let scheduler_result = RuntimeCall::Scheduler(pallet_scheduler::Call::cancel {
when: 100,
index: 0,
})
.dispatch(RuntimeOrigin::root());
match scheduler_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Scheduler calls should not be filtered by safe mode"
);
}
}
// Test Referenda pallet - core OpenGov proposal system
let referenda_result =
RuntimeCall::Referenda(pallet_referenda::Call::cancel { index: 0 })
.dispatch(RuntimeOrigin::root());
match referenda_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"Referenda calls should not be filtered by safe mode"
);
}
}
// Test ConvictionVoting - allows token holders to vote during emergencies
let voting_result = RuntimeCall::ConvictionVoting(
pallet_conviction_voting::Call::remove_other_vote {
target: account_id(BOB),
class: 0,
index: 0,
},
)
.dispatch(RuntimeOrigin::signed(account_id(ALICE)));
match voting_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"ConvictionVoting calls should not be filtered by safe mode"
);
}
}
// Test TechnicalCommittee - expert oversight for emergency actions
let tech_committee_result =
RuntimeCall::TechnicalCommittee(pallet_collective::Call::set_members {
new_members: vec![account_id(ALICE)],
prime: None,
old_count: 0,
})
.dispatch(RuntimeOrigin::root());
match tech_committee_result {
Ok(_) => {}
Err(e) => {
let call_filtered_error: sp_runtime::DispatchError =
frame_system::Error::<Runtime>::CallFiltered.into();
assert_ne!(
format!("{:?}", e.error),
format!("{:?}", call_filtered_error),
"TechnicalCommittee calls should not be filtered by safe mode"
);
}
}
});
}
#[test]
fn error_surface_consistency() {
ExtBuilder::default()
.with_sudo(account_id(ALICE))
.with_balances(vec![(account_id(ALICE), 1_000_000)])
.build()
.execute_with(|| {
// Activate both restrictions
assert_ok!(
RuntimeCall::SafeMode(pallet_safe_mode::Call::force_enter {})
.dispatch(RuntimeOrigin::root())
);
let call = transfer_call(100);
let call_name = call_name(&call);
assert_ok!(RuntimeCall::TxPause(pallet_tx_pause::Call::pause {
full_name: call_name,
})
.dispatch(RuntimeOrigin::root()));
// Validate the blocked call - should return consistent error
let xt = UncheckedExtrinsic::new_bare(call.clone().into());
let validation_result = Runtime::validate_transaction(
TransactionSource::External,
xt,
Default::default(),
);
// Should return InvalidTransaction::Call
assert_eq!(
validation_result,
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
);
// Dispatch should also fail with consistent error
assert_noop!(
call.dispatch(RuntimeOrigin::signed(account_id(ALICE))),
frame_system::Error::<Runtime>::CallFiltered
);
});
}
}

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.10311212470204876148",
"version": "0.1.0-autogenerated.11494808361211823293",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.