Skip to main content

orml_gradually_update/
lib.rs

1//! # Gradually Update
2//! A module for scheduling gradually updates to storage values.
3//!
4//! - [`Config`](./trait.Config.html)
5//! - [`Call`](./enum.Call.html)
6//! - [`Module`](./struct.Module.html)
7//!
8//! ## Overview
9//!
10//! This module exposes capabilities for scheduling updates to storage values
11//! gradually. This is useful to change parameter values gradually to ensure a
12//! smooth transition. It is also possible to cancel an update before it reaches
13//! to target value.
14//!
15//! NOTE: Only unsigned integer value up to 128 bits are supported. But a
16//! "newtype" pattern struct that wraps an unsigned integer works too such as
17//! `Permill` and `FixedU128`.
18
19#![cfg_attr(not(feature = "std"), no_std)]
20// Disable the following two lints since they originate from an external macro (namely decl_storage)
21#![allow(clippy::string_lit_as_bytes)]
22#![allow(clippy::unused_unit)]
23
24use frame_support::{
25	ensure,
26	pallet_prelude::*,
27	storage,
28	traits::{EnsureOrigin, Get},
29	BoundedVec,
30};
31use frame_system::pallet_prelude::*;
32use parity_scale_codec::MaxEncodedLen;
33use scale_info::TypeInfo;
34use sp_runtime::{
35	traits::{SaturatedConversion, Saturating},
36	DispatchResult, RuntimeDebug,
37};
38
39mod default_weight;
40mod mock;
41mod tests;
42
43/// Gradually update a value stored at `key` to `target_value`,
44/// change `per_block` * `T::UpdateFrequency` per `T::UpdateFrequency`
45/// blocks.
46#[derive(Encode, Decode, Clone, Eq, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo, DecodeWithMemTracking)]
47pub struct GraduallyUpdate<Key, Value> {
48	pub key: Key,
49	pub target_value: Value,
50	pub per_block: Value,
51}
52
53pub use module::*;
54
55#[frame_support::pallet]
56pub mod module {
57	use super::*;
58
59	pub trait WeightInfo {
60		fn gradually_update() -> Weight;
61		fn cancel_gradually_update() -> Weight;
62		fn on_finalize(u: u32) -> Weight;
63	}
64
65	pub(crate) type StorageKeyBytes<T> = BoundedVec<u8, <T as Config>::MaxStorageKeyBytes>;
66	pub(crate) type StorageValueBytes<T> = BoundedVec<u8, <T as Config>::MaxStorageValueBytes>;
67
68	type GraduallyUpdateOf<T> = GraduallyUpdate<StorageKeyBytes<T>, StorageValueBytes<T>>;
69
70	#[pallet::config]
71	pub trait Config: frame_system::Config {
72		/// The frequency of updating values between blocks
73		#[pallet::constant]
74		type UpdateFrequency: Get<BlockNumberFor<Self>>;
75
76		/// The origin that can schedule an update
77		type DispatchOrigin: EnsureOrigin<Self::RuntimeOrigin>;
78
79		/// Weight information for extrinsics in this module.
80		type WeightInfo: WeightInfo;
81
82		/// Maximum active gradual updates
83		type MaxGraduallyUpdate: Get<u32>;
84
85		/// Maximum size of storage key
86		type MaxStorageKeyBytes: Get<u32>;
87
88		/// Maximum size of storage value
89		type MaxStorageValueBytes: Get<u32>;
90	}
91
92	#[pallet::error]
93	pub enum Error<T> {
94		/// The `per_block` or `target_value` is invalid.
95		InvalidPerBlockOrTargetValue,
96		/// The `target_value` is invalid.
97		InvalidTargetValue,
98		/// Another update is already been scheduled for this key.
99		GraduallyUpdateHasExisted,
100		/// No update exists to cancel.
101		GraduallyUpdateNotFound,
102		/// Maximum updates exceeded
103		MaxGraduallyUpdateExceeded,
104		/// Maximum key size exceeded
105		MaxStorageKeyBytesExceeded,
106		/// Maximum value size exceeded
107		MaxStorageValueBytesExceeded,
108	}
109
110	#[pallet::event]
111	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
112	pub enum Event<T: Config> {
113		/// Gradually update added.
114		GraduallyUpdateAdded {
115			key: StorageKeyBytes<T>,
116			per_block: StorageValueBytes<T>,
117			target_value: StorageValueBytes<T>,
118		},
119		/// Gradually update cancelled.
120		GraduallyUpdateCancelled { key: StorageKeyBytes<T> },
121		/// Gradually update applied.
122		Updated {
123			block_number: BlockNumberFor<T>,
124			key: StorageKeyBytes<T>,
125			target_value: StorageValueBytes<T>,
126		},
127	}
128
129	/// All the on-going updates
130	#[pallet::storage]
131	#[pallet::getter(fn gradually_updates)]
132	pub(crate) type GraduallyUpdates<T: Config> =
133		StorageValue<_, BoundedVec<GraduallyUpdateOf<T>, T::MaxGraduallyUpdate>, ValueQuery>;
134
135	/// The last updated block number
136	#[pallet::storage]
137	#[pallet::getter(fn last_updated_at)]
138	pub(crate) type LastUpdatedAt<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
139
140	#[pallet::pallet]
141	pub struct Pallet<T>(_);
142
143	#[pallet::hooks]
144	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
145		/// `on_initialize` to return the weight used in `on_finalize`.
146		fn on_initialize(now: BlockNumberFor<T>) -> Weight {
147			if Self::_need_update(now) {
148				T::WeightInfo::on_finalize(GraduallyUpdates::<T>::get().len() as u32)
149			} else {
150				Weight::zero()
151			}
152		}
153
154		/// Update gradually_update to adjust numeric parameter.
155		fn on_finalize(now: BlockNumberFor<T>) {
156			Self::_on_finalize(now);
157		}
158	}
159
160	#[pallet::call]
161	impl<T: Config> Pallet<T> {
162		/// Add gradually_update to adjust numeric parameter.
163		#[pallet::call_index(0)]
164		#[pallet::weight(T::WeightInfo::gradually_update())]
165		pub fn gradually_update(origin: OriginFor<T>, update: GraduallyUpdateOf<T>) -> DispatchResult {
166			T::DispatchOrigin::try_origin(origin).map(|_| ()).or_else(ensure_root)?;
167
168			// Support max value is u128, ensure per_block and target_value <= 16 bytes.
169			ensure!(
170				update.per_block.len() == update.target_value.len() && update.per_block.len() <= 16,
171				Error::<T>::InvalidPerBlockOrTargetValue
172			);
173
174			if storage::unhashed::exists(&update.key) {
175				let current_value = storage::unhashed::get::<StorageValueBytes<T>>(&update.key).unwrap();
176				ensure!(
177					current_value.len() == update.target_value.len(),
178					Error::<T>::InvalidTargetValue
179				);
180			}
181
182			GraduallyUpdates::<T>::try_mutate(|gradually_updates| -> DispatchResult {
183				ensure!(
184					!gradually_updates.contains(&update),
185					Error::<T>::GraduallyUpdateHasExisted
186				);
187
188				gradually_updates
189					.try_push(update.clone())
190					.map_err(|_| Error::<T>::MaxGraduallyUpdateExceeded)?;
191
192				Ok(())
193			})?;
194
195			Self::deposit_event(Event::GraduallyUpdateAdded {
196				key: update.key,
197				per_block: update.per_block,
198				target_value: update.target_value,
199			});
200			Ok(())
201		}
202
203		/// Cancel gradually_update to adjust numeric parameter.
204		#[pallet::call_index(1)]
205		#[pallet::weight(T::WeightInfo::cancel_gradually_update())]
206		pub fn cancel_gradually_update(origin: OriginFor<T>, key: StorageKeyBytes<T>) -> DispatchResult {
207			T::DispatchOrigin::try_origin(origin).map(|_| ()).or_else(ensure_root)?;
208
209			GraduallyUpdates::<T>::try_mutate(|gradually_updates| -> DispatchResult {
210				let old_len = gradually_updates.len();
211				gradually_updates.retain(|item| item.key != key);
212
213				ensure!(gradually_updates.len() != old_len, Error::<T>::GraduallyUpdateNotFound);
214
215				Ok(())
216			})?;
217
218			Self::deposit_event(Event::GraduallyUpdateCancelled { key });
219			Ok(())
220		}
221	}
222}
223
224impl<T: Config> Pallet<T> {
225	fn _need_update(now: BlockNumberFor<T>) -> bool {
226		now >= Self::last_updated_at().saturating_add(T::UpdateFrequency::get())
227	}
228
229	fn _on_finalize(now: BlockNumberFor<T>) {
230		if !Self::_need_update(now) {
231			return;
232		}
233
234		let mut gradually_updates = GraduallyUpdates::<T>::get();
235		let initial_count = gradually_updates.len();
236
237		gradually_updates.retain(|update| {
238			let mut keep = true;
239			let current_value = storage::unhashed::get::<StorageValueBytes<T>>(&update.key).unwrap_or_default();
240			let current_value_u128 = u128::from_le_bytes(Self::convert_vec_to_u8(&current_value));
241
242			let frequency_u128: u128 = T::UpdateFrequency::get().saturated_into();
243
244			let step = u128::from_le_bytes(Self::convert_vec_to_u8(&update.per_block));
245			let step_u128 = step.checked_mul(frequency_u128).unwrap();
246
247			let target_u128 = u128::from_le_bytes(Self::convert_vec_to_u8(&update.target_value));
248
249			let new_value_u128 = if current_value_u128 > target_u128 {
250				(current_value_u128.checked_sub(step_u128).unwrap()).max(target_u128)
251			} else {
252				(current_value_u128.checked_add(step_u128).unwrap()).min(target_u128)
253			};
254
255			// current_value equal target_value, remove gradually_update
256			if new_value_u128 == target_u128 {
257				keep = false;
258			}
259
260			let mut value = new_value_u128.encode();
261			value.truncate(update.target_value.len());
262
263			storage::unhashed::put(&update.key, &value);
264
265			let bounded_value: StorageValueBytes<T> = value.to_vec().try_into().unwrap();
266
267			Self::deposit_event(Event::Updated {
268				block_number: now,
269				key: update.key.clone(),
270				target_value: bounded_value,
271			});
272
273			keep
274		});
275
276		// gradually_update has finished. Remove it from GraduallyUpdates.
277		if gradually_updates.len() < initial_count {
278			GraduallyUpdates::<T>::put(gradually_updates);
279		}
280
281		LastUpdatedAt::<T>::put(now);
282	}
283
284	#[allow(clippy::ptr_arg)]
285	fn convert_vec_to_u8(input: &StorageValueBytes<T>) -> [u8; 16] {
286		let mut array: [u8; 16] = [0; 16];
287		for (i, v) in input.iter().enumerate() {
288			array[i] = *v;
289		}
290		array
291	}
292}