diff --git a/Cargo.dev.toml b/Cargo.dev.toml index f2fd32c49e1955e9e0fd4b16379566a2de94aa73..39e5d76344caaffdf63352a150f4e8142cc524de 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -8,4 +8,5 @@ members = [ "currencies", "auction", "vesting", + "gradually-update", ] diff --git a/gradually-update/Cargo.toml b/gradually-update/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..85e21e8fa13c6157c463edf0aaf9f9f3ea18b49f --- /dev/null +++ b/gradually-update/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "orml-gradually-update" +version = "0.0.1" +authors = ["Laminar Developers <hello@laminar.one>"] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } + +frame-support = { git = "https://github.com/paritytech/substrate.git", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate.git", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate.git", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate.git", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate.git", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", default-features = false } + +[dev-dependencies] +orml-utilities = { path = "../utilities", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "sp-io/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", +] diff --git a/gradually-update/src/lib.rs b/gradually-update/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..8aa2aa3d3ace64ee0a8df895b2048dc11ffb1a06 --- /dev/null +++ b/gradually-update/src/lib.rs @@ -0,0 +1,163 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure, storage, traits::Get}; +use frame_system::{self as system, ensure_root}; + +use sp_runtime::{traits::SaturatedConversion, RuntimeDebug}; +use sp_std::prelude::Vec; + +mod mock; +mod tests; + +type StorageKey = Vec<u8>; +type StorageValue = Vec<u8>; + +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] +pub struct GraduallyUpdate { + key: StorageKey, + target_value: StorageValue, + per_block: StorageValue, +} + +pub trait Trait: frame_system::Trait { + type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>; + type UpdateFrequency: Get<Self::BlockNumber>; +} + +decl_storage! { + trait Store for Module<T: Trait> as GraduallyUpdate { + pub GraduallyUpdates get(fn gradually_updates): Vec<GraduallyUpdate>; + pub GraduallyUpdateBlockNumber get(fn gradually_update_block_number): T::BlockNumber; + } +} + +decl_event!( + /// Event for gradually-update module. + pub enum Event<T> where + <T as frame_system::Trait>::BlockNumber, + { + /// Add gradually_update success (key, per_block, target_value) + GraduallyUpdate(StorageKey, StorageValue, StorageValue), + /// Cancel gradually_update success (key) + CancelGraduallyUpdate(StorageKey), + /// Update gradually_update success (blocknum, key, target_value) + GraduallyUpdateBlockNumber(BlockNumber, StorageKey, StorageValue), + } +); + +decl_error! { + /// Error for gradually-update module. + pub enum Error for Module<T: Trait> { + InvalidPerBlockOrTargetValue, + InvalidTargetValue, + GraduallyUpdateHasExisted, + CancelGradullyUpdateNotExisted, + } +} + +decl_module! { + pub struct Module<T: Trait> for enum Call where origin: T::Origin { + type Error = Error<T>; + + fn deposit_event() = default; + + const UpdateFrequency: T::BlockNumber = T::UpdateFrequency::get(); + + /// Add gradually_update to adjust numeric parameter. This is a root call. + pub fn gradually_update(origin, update: GraduallyUpdate) { + ensure_root(origin)?; + + // Support max value is u128, ensure per_block and target_value <= 16 bytes. + ensure!(update.per_block.len() == update.target_value.len() && update.per_block.len() <= 16, Error::<T>::InvalidPerBlockOrTargetValue); + + if storage::unhashed::exists(&update.key) { + let current_value = storage::unhashed::get::<StorageValue>(&update.key).unwrap(); + ensure!(current_value.len() == update.target_value.len(), Error::<T>::InvalidTargetValue); + } + + let mut gradually_updates = GraduallyUpdates::get(); + ensure!(!gradually_updates.contains(&update), Error::<T>::GraduallyUpdateHasExisted); + + gradually_updates.push(update.clone()); + GraduallyUpdates::put(gradually_updates); + + Self::deposit_event(RawEvent::GraduallyUpdate(update.key, update.per_block, update.target_value)); + } + + /// Cancel gradually_update to adjust numeric parameter. This is a root call. + pub fn cancel_gradually_update(origin, key: StorageKey) { + ensure_root(origin)?; + + let gradually_updates: Vec<GraduallyUpdate> = GraduallyUpdates::get() + .into_iter() + .filter(|item| item.key != key) + .collect(); + + ensure!(GraduallyUpdates::decode_len().unwrap_or_default() - gradually_updates.len() == 1, Error::<T>::CancelGradullyUpdateNotExisted); + GraduallyUpdates::put(gradually_updates); + + Self::deposit_event(RawEvent::CancelGraduallyUpdate(key)); + } + + /// Update gradually_update to adjust numeric parameter. + fn on_finalize(now: T::BlockNumber) { + Self::_on_finalize(now); + } + } +} + +impl<T: Trait> Module<T> { + fn _on_finalize(now: T::BlockNumber) { + if now < GraduallyUpdateBlockNumber::<T>::get() + T::UpdateFrequency::get() { + return; + } + + let mut gradually_updates = GraduallyUpdates::get(); + for (i, update) in gradually_updates.clone().iter().enumerate() { + let current_value = storage::unhashed::get::<StorageValue>(&update.key).unwrap_or_default(); + let current_value_u128 = u128::from_le_bytes(Self::convert_vec_to_u8(¤t_value)); + + let frequency_u128: u128 = T::UpdateFrequency::get().saturated_into(); + + let step = u128::from_le_bytes(Self::convert_vec_to_u8(&update.per_block)); + let step_u128 = step.checked_mul(frequency_u128).unwrap(); + + let target_u128 = u128::from_le_bytes(Self::convert_vec_to_u8(&update.target_value)); + + let new_value_u128: u128; + if current_value_u128 > target_u128 { + new_value_u128 = (current_value_u128.checked_sub(step_u128).unwrap()).max(target_u128); + } else { + new_value_u128 = (current_value_u128.checked_add(step_u128).unwrap()).min(target_u128); + } + + // current_value equal target_value, remove gradually_update + if new_value_u128 == target_u128 { + gradually_updates.remove(i); + } + + let mut value = new_value_u128.encode(); + value.truncate(update.target_value.len()); + + storage::unhashed::put(&update.key, &value); + + Self::deposit_event(RawEvent::GraduallyUpdateBlockNumber(now, update.key.clone(), value)); + } + + // gradually_update has finished. Remove it from GraduallyUpdates. + if gradually_updates.len() < GraduallyUpdates::decode_len().unwrap_or_default() { + GraduallyUpdates::put(gradually_updates); + } + + GraduallyUpdateBlockNumber::<T>::put(now); + } + + fn convert_vec_to_u8(input: &StorageValue) -> [u8; 16] { + let mut array: [u8; 16] = [0; 16]; + for (i, v) in input.iter().enumerate() { + array[i] = v.clone(); + } + array + } +} diff --git a/gradually-update/src/mock.rs b/gradually-update/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb35caa8442075c05f9e207461815ecff09d2db5 --- /dev/null +++ b/gradually-update/src/mock.rs @@ -0,0 +1,89 @@ +//! Mocks for the gradually-update module. + +#![cfg(test)] + +use frame_support::{impl_outer_event, impl_outer_origin, parameter_types}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{testing::Header, traits::IdentityLookup, Perbill}; + +use super::*; + +impl_outer_origin! { + pub enum Origin for Runtime {} +} + +mod gradually_update { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Runtime { + frame_system<T>, + gradually_update<T>, + } +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} + +pub type AccountId = u64; +pub type BlockNumber = u64; + +impl frame_system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = (); + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); +} +pub type System = system::Module<Runtime>; + +parameter_types! { + pub const UpdateFrequency: BlockNumber = 10; +} + +impl Trait for Runtime { + type Event = TestEvent; + type UpdateFrequency = UpdateFrequency; +} +pub type GraduallyUpdateModule = Module<Runtime>; + +pub struct ExtBuilder; + +impl Default for ExtBuilder { + fn default() -> Self { + ExtBuilder + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default() + .build_storage::<Runtime>() + .unwrap(); + + t.into() + } +} diff --git a/gradually-update/src/tests.rs b/gradually-update/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..c60ef71cd26582942b6a44506c94a1871def484f --- /dev/null +++ b/gradually-update/src/tests.rs @@ -0,0 +1,324 @@ +//! Unit tests for the gradually-update module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::{ExtBuilder, GraduallyUpdateModule, Origin, Runtime, System, TestEvent}; +use orml_utilities::FixedU128; +use sp_runtime::{traits::OnFinalize, Permill}; + +fn storage_set(key: &Vec<u8>, value: &Vec<u8>) { + storage::unhashed::put(key, value); +} + +fn storage_get(key: &Vec<u8>) -> Vec<u8> { + storage::unhashed::get::<StorageValue>(key).unwrap_or_default() +} + +#[test] +fn gradually_update_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: vec![9], + per_block: vec![1], + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + + let gradually_update_event = TestEvent::gradually_update(RawEvent::GraduallyUpdate( + update.key, + update.per_block, + update.target_value, + )); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_event)); + }); +} + +#[test] +fn gradually_update_should_fail() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: 9u32.encode(), + per_block: 1u64.encode(), + }; + assert_noop!( + GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone()), + Error::<Runtime>::InvalidPerBlockOrTargetValue + ); + + let update = GraduallyUpdate { + key: vec![1], + target_value: 90u32.encode(), + per_block: 1u32.encode(), + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + + GraduallyUpdateModule::on_finalize(20); + + let new_update = GraduallyUpdate { + key: vec![1], + target_value: 9u64.encode(), + per_block: 1u64.encode(), + }; + assert_noop!( + GraduallyUpdateModule::gradually_update(Origin::ROOT, new_update.clone()), + Error::<Runtime>::InvalidTargetValue + ); + + assert_noop!( + GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone()), + Error::<Runtime>::GraduallyUpdateHasExisted + ); + }); +} + +#[test] +fn cancel_gradually_update_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: vec![9], + per_block: vec![1], + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + let gradually_update_event = TestEvent::gradually_update(RawEvent::GraduallyUpdate( + update.key.clone(), + update.per_block, + update.target_value, + )); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_event)); + + assert_ok!(GraduallyUpdateModule::cancel_gradually_update( + Origin::ROOT, + update.key.clone() + )); + let cancel_gradually_update_event = TestEvent::gradually_update(RawEvent::CancelGraduallyUpdate(update.key)); + assert!(System::events() + .iter() + .any(|record| record.event == cancel_gradually_update_event)); + }); +} + +#[test] +fn cancel_gradually_update_should_fail() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: 9u32.encode(), + per_block: 1u32.encode(), + }; + assert_noop!( + GraduallyUpdateModule::cancel_gradually_update(Origin::ROOT, update.key.clone()), + Error::<Runtime>::CancelGradullyUpdateNotExisted + ); + + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + + assert_ok!(GraduallyUpdateModule::cancel_gradually_update( + Origin::ROOT, + update.key.clone() + )); + }); +} + +#[test] +fn add_on_finalize_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: vec![30], + per_block: vec![1], + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + assert_eq!(storage_get(&update.key), vec![]); + + GraduallyUpdateModule::on_finalize(10); + assert_eq!(storage_get(&update.key), vec![10]); + let gradually_update_blocknumber_event = + TestEvent::gradually_update(RawEvent::GraduallyUpdateBlockNumber(10, update.key.clone(), vec![10])); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_blocknumber_event)); + assert_eq!(System::events().len(), 2); + + GraduallyUpdateModule::on_finalize(15); + assert_eq!(storage_get(&update.key), vec![10]); + assert_eq!(System::events().len(), 2); + + GraduallyUpdateModule::on_finalize(20); + assert_eq!(storage_get(&update.key), vec![20]); + let gradually_update_blocknumber_event = + TestEvent::gradually_update(RawEvent::GraduallyUpdateBlockNumber(20, update.key.clone(), vec![20])); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_blocknumber_event)); + assert_eq!(System::events().len(), 3); + + GraduallyUpdateModule::on_finalize(40); + assert_eq!(storage_get(&update.key), vec![30]); + let gradually_update_blocknumber_event = + TestEvent::gradually_update(RawEvent::GraduallyUpdateBlockNumber(40, update.key.clone(), vec![30])); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_blocknumber_event)); + }); +} + +#[test] +fn sub_on_finalize_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: vec![5], + per_block: vec![1], + }; + + storage_set(&update.key, &vec![30]); + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + assert_eq!(storage_get(&update.key), vec![30]); + + GraduallyUpdateModule::on_finalize(10); + assert_eq!(storage_get(&update.key), vec![20]); + let gradually_update_blocknumber_event = + TestEvent::gradually_update(RawEvent::GraduallyUpdateBlockNumber(10, update.key.clone(), vec![20])); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_blocknumber_event)); + assert_eq!(System::events().len(), 2); + + GraduallyUpdateModule::on_finalize(15); + assert_eq!(storage_get(&update.key), vec![20]); + assert_eq!(System::events().len(), 2); + + GraduallyUpdateModule::on_finalize(20); + assert_eq!(storage_get(&update.key), vec![10]); + let gradually_update_blocknumber_event = + TestEvent::gradually_update(RawEvent::GraduallyUpdateBlockNumber(20, update.key.clone(), vec![10])); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_blocknumber_event)); + assert_eq!(System::events().len(), 3); + + GraduallyUpdateModule::on_finalize(40); + assert_eq!(storage_get(&update.key), vec![5]); + let gradually_update_blocknumber_event = + TestEvent::gradually_update(RawEvent::GraduallyUpdateBlockNumber(40, update.key.clone(), vec![5])); + assert!(System::events() + .iter() + .any(|record| record.event == gradually_update_blocknumber_event)); + }); +} + +#[test] +fn u32_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: 30u32.encode(), + per_block: 1u32.encode(), + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + assert_eq!(storage_get(&update.key), vec![]); + GraduallyUpdateModule::on_finalize(10); + assert_eq!(storage_get(&update.key), vec![10, 0, 0, 0]); + GraduallyUpdateModule::on_finalize(15); + assert_eq!(storage_get(&update.key), vec![10, 0, 0, 0]); + GraduallyUpdateModule::on_finalize(20); + assert_eq!(storage_get(&update.key), vec![20, 0, 0, 0]); + GraduallyUpdateModule::on_finalize(40); + assert_eq!(storage_get(&update.key), vec![30, 0, 0, 0]); + }); +} + +#[test] +fn u128_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: 30u128.encode(), + per_block: 1u128.encode(), + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + assert_eq!(storage_get(&update.key), vec![]); + GraduallyUpdateModule::on_finalize(10); + assert_eq!( + storage_get(&update.key), + vec![10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + GraduallyUpdateModule::on_finalize(15); + assert_eq!( + storage_get(&update.key), + vec![10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + GraduallyUpdateModule::on_finalize(20); + assert_eq!( + storage_get(&update.key), + vec![20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + GraduallyUpdateModule::on_finalize(40); + assert_eq!( + storage_get(&update.key), + vec![30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + }); +} + +#[test] +fn permill_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: Permill::from_percent(30).encode(), + per_block: Permill::from_percent(1).encode(), + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + assert_eq!(storage_get(&update.key), vec![]); + GraduallyUpdateModule::on_finalize(10); + assert_eq!(storage_get(&update.key), vec![160, 134, 1, 0]); + GraduallyUpdateModule::on_finalize(15); + assert_eq!(storage_get(&update.key), vec![160, 134, 1, 0]); + GraduallyUpdateModule::on_finalize(20); + assert_eq!(storage_get(&update.key), vec![64, 13, 3, 0]); + GraduallyUpdateModule::on_finalize(40); + assert_eq!(storage_get(&update.key), vec![224, 147, 4, 0]); + }); +} + +#[test] +fn fixedu128_should_work() { + ExtBuilder::default().build().execute_with(|| { + let update = GraduallyUpdate { + key: vec![1], + target_value: FixedU128::from_rational(30, 1).encode(), + per_block: FixedU128::from_rational(1, 1).encode(), + }; + assert_ok!(GraduallyUpdateModule::gradually_update(Origin::ROOT, update.clone())); + assert_eq!(storage_get(&update.key), vec![]); + GraduallyUpdateModule::on_finalize(10); + assert_eq!( + storage_get(&update.key), + vec![0, 0, 232, 137, 4, 35, 199, 138, 0, 0, 0, 0, 0, 0, 0, 0] + ); + GraduallyUpdateModule::on_finalize(15); + assert_eq!( + storage_get(&update.key), + vec![0, 0, 232, 137, 4, 35, 199, 138, 0, 0, 0, 0, 0, 0, 0, 0] + ); + GraduallyUpdateModule::on_finalize(20); + assert_eq!( + storage_get(&update.key), + vec![0, 0, 208, 19, 9, 70, 142, 21, 1, 0, 0, 0, 0, 0, 0, 0] + ); + GraduallyUpdateModule::on_finalize(40); + assert_eq!( + storage_get(&update.key), + vec![0, 0, 184, 157, 13, 105, 85, 160, 1, 0, 0, 0, 0, 0, 0, 0] + ); + }); +}