1
// Copyright 2019-2022 Moonsong Labs
2
// This file is part of Moonkit.
3

            
4
// Moonkit is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Moonkit is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Moonkit.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! A pallet to put your runtime into a restricted maintenance or safe mode. This is useful when
18
//! performing site maintenance, running data migrations, or protecting the chain during an attack.
19
//!
20
//! This introduces one storage read to fetch the base filter for each extrinsic. However, it should
21
//! be that the state cache eliminates this cost almost entirely. I wonder if that can or should be
22
//! reflected in the weight calculation.
23
//!
24
//! Possible future improvements
25
//! 1. This could be more configurable by letting the runtime developer specify a type (probably an
26
//! enum) that can be converted into a filter. Similar end result (but different implementation) as
27
//! Acala has it
28
//! github.com/AcalaNetwork/Acala/blob/pause-transaction/modules/transaction-pause/src/lib.rs#L71
29
//!
30
//! 2. Automatically enable maintenance mode after a long timeout is detected between blocks.
31
//! To implement this we would couple to the timestamp pallet and store the timestamp of the
32
//! previous block.
33
//!
34
//! 3. Different origins for entering and leaving maintenance mode.
35
//!
36
//! 4. Maintenance mode timeout. To avoid getting stuck in maintenance mode. It could automatically
37
//! switch back to normal mode after a pre-decided number of blocks. Maybe there could be an
38
//! extrinsic to extend the maintenance time.
39

            
40
#![allow(non_camel_case_types)]
41
#![cfg_attr(not(feature = "std"), no_std)]
42

            
43
#[cfg(test)]
44
mod mock;
45
#[cfg(test)]
46
mod tests;
47

            
48
use frame_support::pallet;
49

            
50
pub use pallet::*;
51

            
52
#[pallet]
53
pub mod pallet {
54
	#[cfg(feature = "xcm-support")]
55
	use frame_support::pallet_prelude::*;
56
	use frame_support::traits::{BuildGenesisConfig, Contains, EnsureOrigin, QueuePausedQuery};
57
	use frame_system::pallet_prelude::*;
58
	#[cfg(feature = "xcm-support")]
59
	use xcm_primitives::PauseXcmExecution;
60

            
61
	/// Pallet for migrations
62
	#[pallet::pallet]
63
	#[pallet::without_storage_info]
64
	pub struct Pallet<T>(PhantomData<T>);
65

            
66
	/// Configuration trait of this pallet.
67
	#[pallet::config]
68
	pub trait Config: frame_system::Config {
69
		/// Overarching event type
70
		type RuntimeEvent: From<Event> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
71
		/// The base call filter to be used in normal operating mode
72
		/// (When we aren't in the middle of a migration)
73
		type NormalCallFilter: Contains<Self::RuntimeCall>;
74
		/// The base call filter to be used when we are in the middle of migrations
75
		/// This should be very restrictive. Probably not allowing anything except possibly
76
		/// something like sudo or other emergency processes
77
		type MaintenanceCallFilter: Contains<Self::RuntimeCall>;
78
		/// The origin from which the call to enter or exit maintenance mode must come
79
		/// Take care when choosing your maintenance call filter to ensure that you'll still be
80
		/// able to return to normal mode. For example, if your MaintenanceOrigin is a council, make
81
		/// sure that your councilors can still cast votes.
82
		type MaintenanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
83
		/// Handler to suspend and resume XCM execution
84
		#[cfg(feature = "xcm-support")]
85
		type XcmExecutionManager: PauseXcmExecution;
86
	}
87

            
88
	#[pallet::event]
89
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
90
	pub enum Event {
91
		/// The chain was put into Maintenance Mode
92
		EnteredMaintenanceMode,
93
		/// The chain returned to its normal operating state
94
		NormalOperationResumed,
95
		/// The call to suspend on_idle XCM execution failed with inner error
96
		FailedToSuspendIdleXcmExecution { error: DispatchError },
97
		/// The call to resume on_idle XCM execution failed with inner error
98
		FailedToResumeIdleXcmExecution { error: DispatchError },
99
	}
100

            
101
	/// An error that can occur while executing this pallet's extrinsics.
102
	#[pallet::error]
103
	pub enum Error<T> {
104
		/// The chain cannot enter maintenance mode because it is already in maintenance mode
105
		AlreadyInMaintenanceMode,
106
		/// The chain cannot resume normal operation because it is not in maintenance mode
107
		NotInMaintenanceMode,
108
	}
109

            
110
	#[pallet::storage]
111
	#[pallet::getter(fn maintenance_mode)]
112
	/// Whether the site is in maintenance mode
113
	type MaintenanceMode<T: Config> = StorageValue<_, bool, ValueQuery>;
114

            
115
	#[pallet::call]
116
	impl<T: Config> Pallet<T> {
117
		/// Place the chain in maintenance mode
118
		///
119
		/// Weight cost is:
120
		/// * One DB read to ensure we're not already in maintenance mode
121
		/// * Three DB writes - 1 for the mode, 1 for suspending xcm execution, 1 for the event
122
		#[pallet::call_index(0)]
123
		#[pallet::weight(T::DbWeight::get().read + 3 * T::DbWeight::get().write)]
124
		pub fn enter_maintenance_mode(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
125
			// Ensure Origin
126
			T::MaintenanceOrigin::ensure_origin(origin)?;
127

            
128
			// Ensure we're not aleady in maintenance mode.
129
			// This test is not strictly necessary, but seeing the error may help a confused chain
130
			// operator during an emergency
131
			ensure!(
132
				!MaintenanceMode::<T>::get(),
133
				Error::<T>::AlreadyInMaintenanceMode
134
			);
135

            
136
			// Write to storage
137
			MaintenanceMode::<T>::put(true);
138
			// Suspend XCM execution
139
			#[cfg(feature = "xcm-support")]
140
			if let Err(error) = T::XcmExecutionManager::suspend_xcm_execution() {
141
				<Pallet<T>>::deposit_event(Event::FailedToSuspendIdleXcmExecution { error });
142
			}
143

            
144
			// Event
145
			<Pallet<T>>::deposit_event(Event::EnteredMaintenanceMode);
146

            
147
			Ok(().into())
148
		}
149

            
150
		/// Return the chain to normal operating mode
151
		///
152
		/// Weight cost is:
153
		/// * One DB read to ensure we're in maintenance mode
154
		/// * Three DB writes - 1 for the mode, 1 for resuming xcm execution, 1 for the event
155
		#[pallet::call_index(1)]
156
		#[pallet::weight(T::DbWeight::get().read + 3 * T::DbWeight::get().write)]
157
		pub fn resume_normal_operation(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
158
			// Ensure Origin
159
			T::MaintenanceOrigin::ensure_origin(origin)?;
160

            
161
			// Ensure we're actually in maintenance mode.
162
			// This test is not strictly necessary, but seeing the error may help a confused chain
163
			// operator during an emergency
164
			ensure!(
165
				MaintenanceMode::<T>::get(),
166
				Error::<T>::NotInMaintenanceMode
167
			);
168

            
169
			// Write to storage
170
			MaintenanceMode::<T>::put(false);
171
			// Resume XCM execution
172
			#[cfg(feature = "xcm-support")]
173
			if let Err(error) = T::XcmExecutionManager::resume_xcm_execution() {
174
				<Pallet<T>>::deposit_event(Event::FailedToResumeIdleXcmExecution { error });
175
			}
176

            
177
			// Event
178
			<Pallet<T>>::deposit_event(Event::NormalOperationResumed);
179

            
180
			Ok(().into())
181
		}
182
	}
183

            
184
	#[derive(frame_support::DefaultNoBound)]
185
	#[pallet::genesis_config]
186
	/// Genesis config for maintenance mode pallet
187
	pub struct GenesisConfig<T: Config> {
188
		/// Whether to launch in maintenance mode
189
		pub start_in_maintenance_mode: bool,
190
		#[serde(skip)]
191
		pub _config: sp_std::marker::PhantomData<T>,
192
	}
193

            
194
	#[pallet::genesis_build]
195
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
196
		fn build(&self) {
197
			if self.start_in_maintenance_mode {
198
				MaintenanceMode::<T>::put(true);
199
			}
200
		}
201
	}
202

            
203
	impl<T: Config> Contains<T::RuntimeCall> for Pallet<T> {
204
		fn contains(call: &T::RuntimeCall) -> bool {
205
			if MaintenanceMode::<T>::get() {
206
				T::MaintenanceCallFilter::contains(call)
207
			} else {
208
				T::NormalCallFilter::contains(call)
209
			}
210
		}
211
	}
212

            
213
	#[cfg(feature = "xcm-support")]
214
	impl<T: Config, Origin> QueuePausedQuery<Origin> for Pallet<T> {
215
		fn is_paused(_origin: &Origin) -> bool {
216
			MaintenanceMode::<T>::get()
217
		}
218
	}
219
}