git.delta.rocks / unique-network / refs/commits / 67f20da6affd

difftreelog

Orml-vesting fix

str-mv2021-11-22parent: #51e104f.patch.diff
in: master

8 files changed

addedpallets/orml-vesting/ README.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/ README.md
@@ -0,0 +1,9 @@
+# Vesting Module
+
+## Overview
+
+Vesting module provides a means of scheduled balance lock on an account. It uses the *graded vesting* way, which unlocks a specific amount of balance every period of time, until all balance unlocked.
+
+### Vesting Schedule
+
+The schedule of a vesting is described by data structure `VestingSchedule`: from the block number of `start`, for every `period` amount of blocks, `per_period` amount of balance would unlocked, until number of periods `period_count` reached. Note in vesting schedules, *time* is measured by block number. All `VestingSchedule`s under an account could be queried in chain state.
addedpallets/orml-vesting/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "orml-vesting"
+description = "Provides scheduled balance locking mechanism, in a *graded vesting* way."
+license = "Apache-2.0"
+version = "0.4.1-dev"
+authors = ["Laminar Developers <hello@laminar.one>"]
+edition = "2018"
+
+[dependencies]
+scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+serde = { version = "1.0.124", optional = true }
+codec = { package = "parity-scale-codec", version = "2.3.1", default-features = false, features = ["max-encoded-len"] }
+sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false }
+sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false }
+sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false }
+
+frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false }
+frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false }
+
+[dev-dependencies]
+sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12" }
+pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12" }
+
+[features]
+default = ["std"]
+std = [
+	"serde",
+	"codec/std",
+	"scale-info/std",
+	"sp-runtime/std",
+	"sp-std/std",
+	"sp-io/std",
+	"frame-support/std",
+	"frame-system/std",
+]
+runtime-benchmarks = [
+	"frame-support/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+]
+try-runtime = ["frame-support/try-runtime"]
addedpallets/orml-vesting/src/default_weight.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/src/default_weight.rs
@@ -0,0 +1,27 @@
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
+
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(clippy::unnecessary_cast)]
+
+use frame_support::weights::{constants::RocksDbWeight as DbWeight, Weight};
+
+impl crate::WeightInfo for () {
+	fn vested_transfer() -> Weight {
+		(310_862_000 as Weight)
+			.saturating_add(DbWeight::get().reads(4 as Weight))
+			.saturating_add(DbWeight::get().writes(4 as Weight))
+	}
+	fn claim(i: u32) -> Weight {
+		(158_614_000 as Weight)
+			.saturating_add((958_000 as Weight).saturating_mul(i as Weight))
+			.saturating_add(DbWeight::get().reads(3 as Weight))
+			.saturating_add(DbWeight::get().writes(3 as Weight))
+	}
+	fn update_vesting_schedules(i: u32) -> Weight {
+		(119_811_000 as Weight)
+			.saturating_add((2_320_000 as Weight).saturating_mul(i as Weight))
+			.saturating_add(DbWeight::get().reads(2 as Weight))
+			.saturating_add(DbWeight::get().writes(3 as Weight))
+	}
+}
addedpallets/orml-vesting/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/src/lib.rs
@@ -0,0 +1,380 @@
+//! # Vesting Module
+//!
+//! ## Overview
+//!
+//! Vesting module provides a means of scheduled balance lock on an account. It
+//! uses the *graded vesting* way, which unlocks a specific amount of balance
+//! every period of time, until all balance unlocked.
+//!
+//! ### Vesting Schedule
+//!
+//! The schedule of a vesting is described by data structure `VestingSchedule`:
+//! from the block number of `start`, for every `period` amount of blocks,
+//! `per_period` amount of balance would unlocked, until number of periods
+//! `period_count` reached. Note in vesting schedules, *time* is measured by
+//! block number. All `VestingSchedule`s under an account could be queried in
+//! chain state.
+//!
+//! ## Interface
+//!
+//! ### Dispatchable Functions
+//!
+//! - `vested_transfer` - Add a new vesting schedule for an account.
+//! - `claim` - Claim unlocked balances.
+//! - `update_vesting_schedules` - Update all vesting schedules under an
+//!   account, `root` origin required.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![allow(clippy::unused_unit)]
+
+use codec::{HasCompact, MaxEncodedLen};
+use frame_support::{
+	ensure,
+	pallet_prelude::*,
+	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, WithdrawReasons},
+	transactional, BoundedVec,
+};
+use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
+use scale_info::TypeInfo;
+use sp_runtime::{
+	traits::{AtLeast32Bit, BlockNumberProvider, CheckedAdd, Saturating, StaticLookup, Zero},
+	ArithmeticError, DispatchResult, RuntimeDebug,
+};
+use sp_std::{
+	cmp::{Eq, PartialEq},
+	convert::TryInto,
+	vec::Vec,
+};
+
+mod mock;
+mod tests;
+mod weights;
+
+pub use module::*;
+pub use weights::WeightInfo;
+
+pub const VESTING_LOCK_ID: LockIdentifier = *b"ormlvest";
+
+/// The vesting schedule.
+///
+/// Benefits would be granted gradually, `per_period` amount every `period`
+/// of blocks after `start`.
+#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
+pub struct VestingSchedule<BlockNumber, Balance: HasCompact> {
+	/// Vesting starting block
+	pub start: BlockNumber,
+	/// Number of blocks between vest
+	pub period: BlockNumber,
+	/// Number of vest
+	pub period_count: u32,
+	/// Amount of tokens to release per vest
+	#[codec(compact)]
+	pub per_period: Balance,
+}
+
+impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + Copy> VestingSchedule<BlockNumber, Balance> {
+	/// Returns the end of all periods, `None` if calculation overflows.
+	pub fn end(&self) -> Option<BlockNumber> {
+		// period * period_count + start
+		self.period
+			.checked_mul(&self.period_count.into())?
+			.checked_add(&self.start)
+	}
+
+	/// Returns all locked amount, `None` if calculation overflows.
+	pub fn total_amount(&self) -> Option<Balance> {
+		self.per_period.checked_mul(&self.period_count.into())
+	}
+
+	/// Returns locked amount for a given `time`.
+	///
+	/// Note this func assumes schedule is a valid one(non-zero period and
+	/// non-overflow total amount), and it should be guaranteed by callers.
+	pub fn locked_amount(&self, time: BlockNumber) -> Balance {
+		// full = (time - start) / period
+		// unrealized = period_count - full
+		// per_period * unrealized
+		let full = time
+			.saturating_sub(self.start)
+			.checked_div(&self.period)
+			.expect("ensured non-zero period; qed");
+		let unrealized = self.period_count.saturating_sub(full.unique_saturated_into());
+		self.per_period
+			.checked_mul(&unrealized.into())
+			.expect("ensured non-overflow total amount; qed")
+	}
+}
+
+#[frame_support::pallet]
+pub mod module {
+	use super::*;
+
+	pub(crate) type BalanceOf<T> =
+		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
+	pub(crate) type VestingScheduleOf<T> = VestingSchedule<<T as frame_system::Config>::BlockNumber, BalanceOf<T>>;
+	pub type ScheduledItem<T> = (
+		<T as frame_system::Config>::AccountId,
+		<T as frame_system::Config>::BlockNumber,
+		<T as frame_system::Config>::BlockNumber,
+		u32,
+		BalanceOf<T>,
+	);
+
+	#[pallet::config]
+	pub trait Config: frame_system::Config {
+		type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
+
+		type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
+
+		#[pallet::constant]
+		/// The minimum amount transferred to call `vested_transfer`.
+		type MinVestedTransfer: Get<BalanceOf<Self>>;
+
+		/// Required origin for vested transfer.
+		type VestedTransferOrigin: EnsureOrigin<Self::Origin, Success = Self::AccountId>;
+
+		/// Weight information for extrinsics in this module.
+		type WeightInfo: WeightInfo;
+
+		/// The maximum vesting schedules
+		type MaxVestingSchedules: Get<u32>;
+
+		// The block number provider
+		type BlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;
+	}
+
+	#[pallet::error]
+	pub enum Error<T> {
+		/// Vesting period is zero
+		ZeroVestingPeriod,
+		/// Number of vests is zero
+		ZeroVestingPeriodCount,
+		/// Insufficient amount of balance to lock
+		InsufficientBalanceToLock,
+		/// This account have too many vesting schedules
+		TooManyVestingSchedules,
+		/// The vested transfer amount is too low
+		AmountLow,
+		/// Failed because the maximum vesting schedules was exceeded
+		MaxVestingSchedulesExceeded,
+	}
+
+	#[pallet::event]
+	#[pallet::generate_deposit(fn deposit_event)]
+	pub enum Event<T: Config> {
+		/// Added new vesting schedule. \[from, to, vesting_schedule\]
+		VestingScheduleAdded(T::AccountId, T::AccountId, VestingScheduleOf<T>),
+		/// Claimed vesting. \[who, locked_amount\]
+		Claimed(T::AccountId, BalanceOf<T>),
+		/// Updated vesting schedules. \[who\]
+		VestingSchedulesUpdated(T::AccountId),
+	}
+
+	/// Vesting schedules of an account.
+	///
+	/// VestingSchedules: map AccountId => Vec<VestingSchedule>
+	#[pallet::storage]
+	#[pallet::getter(fn vesting_schedules)]
+	pub type VestingSchedules<T: Config> = StorageMap<
+		_,
+		Blake2_128Concat,
+		T::AccountId,
+		BoundedVec<VestingScheduleOf<T>, T::MaxVestingSchedules>,
+		ValueQuery,
+	>;
+
+	#[pallet::genesis_config]
+	pub struct GenesisConfig<T: Config> {
+		pub vesting: Vec<ScheduledItem<T>>,
+	}
+
+	#[cfg(feature = "std")]
+	impl<T: Config> Default for GenesisConfig<T> {
+		fn default() -> Self {
+			GenesisConfig { vesting: vec![] }
+		}
+	}
+
+	#[pallet::genesis_build]
+	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
+		fn build(&self) {
+			self.vesting
+				.iter()
+				.for_each(|(who, start, period, period_count, per_period)| {
+					let mut bounded_schedules = VestingSchedules::<T>::get(who);
+					bounded_schedules
+						.try_push(VestingSchedule {
+							start: *start,
+							period: *period,
+							period_count: *period_count,
+							per_period: *per_period,
+						})
+						.expect("Max vesting schedules exceeded");
+					let total_amount = bounded_schedules
+						.iter()
+						.try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(Zero::zero(), |acc_amount, schedule| {
+							let amount = ensure_valid_vesting_schedule::<T>(schedule)?;
+							Ok(acc_amount + amount)
+						})
+						.expect("Invalid vesting schedule");
+
+					assert!(
+						T::Currency::free_balance(who) >= total_amount,
+						"Account do not have enough balance"
+					);
+
+					T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());
+					VestingSchedules::<T>::insert(who, bounded_schedules);
+				});
+		}
+	}
+
+	#[pallet::pallet]
+	pub struct Pallet<T>(_);
+
+	#[pallet::hooks]
+	impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {}
+
+	#[pallet::call]
+	impl<T: Config> Pallet<T> {
+		#[pallet::weight(T::WeightInfo::claim((<T as Config>::MaxVestingSchedules::get() / 2) as u32))]
+		pub fn claim(origin: OriginFor<T>) -> DispatchResult {
+			let who = ensure_signed(origin)?;
+			let locked_amount = Self::do_claim(&who);
+
+			Self::deposit_event(Event::Claimed(who, locked_amount));
+			Ok(())
+		}
+
+		#[pallet::weight(T::WeightInfo::vested_transfer())]
+		pub fn vested_transfer(
+			origin: OriginFor<T>,
+			dest: <T::Lookup as StaticLookup>::Source,
+			schedule: VestingScheduleOf<T>,
+		) -> DispatchResult {
+			let from = T::VestedTransferOrigin::ensure_origin(origin)?;
+			let to = T::Lookup::lookup(dest)?;
+			Self::do_vested_transfer(&from, &to, schedule.clone())?;
+
+			Self::deposit_event(Event::VestingScheduleAdded(from, to, schedule));
+			Ok(())
+		}
+
+		#[pallet::weight(T::WeightInfo::update_vesting_schedules(vesting_schedules.len() as u32))]
+		pub fn update_vesting_schedules(
+			origin: OriginFor<T>,
+			who: <T::Lookup as StaticLookup>::Source,
+			vesting_schedules: Vec<VestingScheduleOf<T>>,
+		) -> DispatchResult {
+			ensure_root(origin)?;
+
+			let account = T::Lookup::lookup(who)?;
+			Self::do_update_vesting_schedules(&account, vesting_schedules)?;
+
+			Self::deposit_event(Event::VestingSchedulesUpdated(account));
+			Ok(())
+		}
+
+		#[pallet::weight(T::WeightInfo::claim((<T as Config>::MaxVestingSchedules::get() / 2) as u32))]
+		pub fn claim_for(origin: OriginFor<T>, dest: <T::Lookup as StaticLookup>::Source) -> DispatchResult {
+			let _ = ensure_signed(origin)?;
+			let who = T::Lookup::lookup(dest)?;
+			let locked_amount = Self::do_claim(&who);
+
+			Self::deposit_event(Event::Claimed(who, locked_amount));
+			Ok(())
+		}
+	}
+}
+
+impl<T: Config> Pallet<T> {
+	fn do_claim(who: &T::AccountId) -> BalanceOf<T> {
+		let locked = Self::locked_balance(who);
+		if locked.is_zero() {
+			// cleanup the storage and unlock the fund
+			<VestingSchedules<T>>::remove(who);
+			T::Currency::remove_lock(VESTING_LOCK_ID, who);
+		} else {
+			T::Currency::set_lock(VESTING_LOCK_ID, who, locked, WithdrawReasons::all());
+		}
+		locked
+	}
+
+	/// Returns locked balance based on current block number.
+	fn locked_balance(who: &T::AccountId) -> BalanceOf<T> {
+		let now = T::BlockNumberProvider::current_block_number();
+		<VestingSchedules<T>>::mutate_exists(who, |maybe_schedules| {
+			let total = if let Some(schedules) = maybe_schedules.as_mut() {
+				let mut total: BalanceOf<T> = Zero::zero();
+				schedules.retain(|s| {
+					let amount = s.locked_amount(now);
+					total = total.saturating_add(amount);
+					!amount.is_zero()
+				});
+				total
+			} else {
+				Zero::zero()
+			};
+			if total.is_zero() {
+				*maybe_schedules = None;
+			}
+			total
+		})
+	}
+
+	#[transactional]
+	fn do_vested_transfer(from: &T::AccountId, to: &T::AccountId, schedule: VestingScheduleOf<T>) -> DispatchResult {
+		let schedule_amount = ensure_valid_vesting_schedule::<T>(&schedule)?;
+
+		let total_amount = Self::locked_balance(to)
+			.checked_add(&schedule_amount)
+			.ok_or(ArithmeticError::Overflow)?;
+
+		T::Currency::transfer(from, to, schedule_amount, ExistenceRequirement::AllowDeath)?;
+		T::Currency::set_lock(VESTING_LOCK_ID, to, total_amount, WithdrawReasons::all());
+		<VestingSchedules<T>>::try_append(to, schedule).map_err(|_| Error::<T>::MaxVestingSchedulesExceeded)?;
+		Ok(())
+	}
+
+	fn do_update_vesting_schedules(who: &T::AccountId, schedules: Vec<VestingScheduleOf<T>>) -> DispatchResult {
+		let bounded_schedules: BoundedVec<VestingScheduleOf<T>, T::MaxVestingSchedules> = schedules
+			.try_into()
+			.map_err(|_| Error::<T>::MaxVestingSchedulesExceeded)?;
+
+		// empty vesting schedules cleanup the storage and unlock the fund
+		if bounded_schedules.len().is_zero() {
+			<VestingSchedules<T>>::remove(who);
+			T::Currency::remove_lock(VESTING_LOCK_ID, who);
+			return Ok(());
+		}
+
+		let total_amount = bounded_schedules
+			.iter()
+			.try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(Zero::zero(), |acc_amount, schedule| {
+				let amount = ensure_valid_vesting_schedule::<T>(schedule)?;
+				Ok(acc_amount + amount)
+			})?;
+		ensure!(
+			T::Currency::free_balance(who) >= total_amount,
+			Error::<T>::InsufficientBalanceToLock,
+		);
+
+		T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());
+		<VestingSchedules<T>>::insert(who, bounded_schedules);
+
+		Ok(())
+	}
+}
+
+/// Returns `Ok(total_total)` if valid schedule, or error.
+fn ensure_valid_vesting_schedule<T: Config>(schedule: &VestingScheduleOf<T>) -> Result<BalanceOf<T>, DispatchError> {
+	ensure!(!schedule.period.is_zero(), Error::<T>::ZeroVestingPeriod);
+	ensure!(!schedule.period_count.is_zero(), Error::<T>::ZeroVestingPeriodCount);
+	ensure!(schedule.end().is_some(), ArithmeticError::Overflow);
+
+	let total_total = schedule.total_amount().ok_or(ArithmeticError::Overflow)?;
+
+	ensure!(total_total >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
+
+	Ok(total_total)
+}
addedpallets/orml-vesting/src/mock.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/src/mock.rs
@@ -0,0 +1,153 @@
+//! Mocks for the vesting module.
+
+#![cfg(test)]
+
+use super::*;
+use frame_support::{
+	construct_runtime, parameter_types,
+	traits::{EnsureOrigin, Everything},
+};
+use frame_system::RawOrigin;
+use sp_core::H256;
+use sp_runtime::{testing::Header, traits::IdentityLookup};
+
+use crate as vesting;
+
+parameter_types! {
+	pub const BlockHashCount: u64 = 250;
+}
+
+pub type AccountId = u128;
+impl frame_system::Config for Runtime {
+	type Origin = Origin;
+	type Call = Call;
+	type Index = u64;
+	type BlockNumber = u64;
+	type Hash = H256;
+	type Hashing = ::sp_runtime::traits::BlakeTwo256;
+	type AccountId = AccountId;
+	type Lookup = IdentityLookup<Self::AccountId>;
+	type Header = Header;
+	type Event = Event;
+	type BlockHashCount = BlockHashCount;
+	type BlockWeights = ();
+	type BlockLength = ();
+	type Version = ();
+	type PalletInfo = PalletInfo;
+	type AccountData = pallet_balances::AccountData<u64>;
+	type OnNewAccount = ();
+	type OnKilledAccount = ();
+	type DbWeight = ();
+	type BaseCallFilter = Everything;
+	type SystemWeightInfo = ();
+	type SS58Prefix = ();
+	type OnSetCode = ();
+}
+
+type Balance = u64;
+
+parameter_types! {
+	pub const ExistentialDeposit: u64 = 1;
+}
+
+impl pallet_balances::Config for Runtime {
+	type Balance = Balance;
+	type DustRemoval = ();
+	type Event = Event;
+	type ExistentialDeposit = ExistentialDeposit;
+	type AccountStore = frame_system::Pallet<Runtime>;
+	type MaxLocks = ();
+	type MaxReserves = ();
+	type ReserveIdentifier = [u8; 8];
+	type WeightInfo = ();
+}
+
+pub struct EnsureAliceOrBob;
+impl EnsureOrigin<Origin> for EnsureAliceOrBob {
+	type Success = AccountId;
+
+	fn try_origin(o: Origin) -> Result<Self::Success, Origin> {
+		Into::<Result<RawOrigin<AccountId>, Origin>>::into(o).and_then(|o| match o {
+			RawOrigin::Signed(ALICE) => Ok(ALICE),
+			RawOrigin::Signed(BOB) => Ok(BOB),
+			r => Err(Origin::from(r)),
+		})
+	}
+
+	#[cfg(feature = "runtime-benchmarks")]
+	fn successful_origin() -> Origin {
+		Origin::from(RawOrigin::Signed(Default::default()))
+	}
+}
+
+parameter_types! {
+	pub const MaxVestingSchedule: u32 = 2;
+	pub const MinVestedTransfer: u64 = 5;
+	pub static MockBlockNumberProvider: u64 = 0;
+}
+
+impl BlockNumberProvider for MockBlockNumberProvider {
+	type BlockNumber = u64;
+
+	fn current_block_number() -> Self::BlockNumber {
+		Self::get()
+	}
+}
+
+impl Config for Runtime {
+	type Event = Event;
+	type Currency = PalletBalances;
+	type MinVestedTransfer = MinVestedTransfer;
+	type VestedTransferOrigin = EnsureAliceOrBob;
+	type WeightInfo = ();
+	type MaxVestingSchedules = MaxVestingSchedule;
+	type BlockNumberProvider = MockBlockNumberProvider;
+}
+
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Runtime>;
+type Block = frame_system::mocking::MockBlock<Runtime>;
+
+construct_runtime!(
+	pub enum Runtime where
+		Block = Block,
+		NodeBlock = Block,
+		UncheckedExtrinsic = UncheckedExtrinsic,
+	{
+		System: frame_system::{Pallet, Call, Storage, Config, Event<T>},
+		Vesting: vesting::{Pallet, Storage, Call, Event<T>, Config<T>},
+		PalletBalances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
+	}
+);
+
+pub const ALICE: AccountId = 1;
+pub const BOB: AccountId = 2;
+pub const CHARLIE: AccountId = 3;
+
+#[derive(Default)]
+pub struct ExtBuilder;
+
+impl ExtBuilder {
+	pub fn build() -> sp_io::TestExternalities {
+		let mut t = frame_system::GenesisConfig::default()
+			.build_storage::<Runtime>()
+			.unwrap();
+
+		pallet_balances::GenesisConfig::<Runtime> {
+			balances: vec![(ALICE, 100), (CHARLIE, 50)],
+		}
+		.assimilate_storage(&mut t)
+		.unwrap();
+
+		vesting::GenesisConfig::<Runtime> {
+			vesting: vec![
+				// who, start, period, period_count, per_period
+				(CHARLIE, 2, 3, 1, 5),
+				(CHARLIE, 2 + 3, 3, 3, 5),
+			],
+		}
+		.assimilate_storage(&mut t)
+		.unwrap();
+
+		t.into()
+	}
+}
addedpallets/orml-vesting/src/test.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/src/test.rs
@@ -0,0 +1,411 @@
+//! Unit tests for the vesting module.
+
+#![cfg(test)]
+
+use super::*;
+use frame_support::{assert_noop, assert_ok, error::BadOrigin};
+use mock::{Event, *};
+use pallet_balances::{BalanceLock, Reasons};
+
+#[test]
+fn vesting_from_chain_spec_works() {
+	ExtBuilder::build().execute_with(|| {
+		assert_ok!(PalletBalances::ensure_can_withdraw(
+			&CHARLIE,
+			10,
+			WithdrawReasons::TRANSFER,
+			20
+		));
+		assert!(PalletBalances::ensure_can_withdraw(&CHARLIE, 11, WithdrawReasons::TRANSFER, 19).is_err());
+
+		assert_eq!(
+			Vesting::vesting_schedules(&CHARLIE),
+			vec![
+				VestingSchedule {
+					start: 2u64,
+					period: 3u64,
+					period_count: 1u32,
+					per_period: 5u64,
+				},
+				VestingSchedule {
+					start: 2u64 + 3u64,
+					period: 3u64,
+					period_count: 3u32,
+					per_period: 5u64,
+				}
+			]
+		);
+
+		MockBlockNumberProvider::set(13);
+
+		assert_ok!(Vesting::claim(Origin::signed(CHARLIE)));
+
+		assert_ok!(PalletBalances::ensure_can_withdraw(
+			&CHARLIE,
+			25,
+			WithdrawReasons::TRANSFER,
+			5
+		));
+		assert!(PalletBalances::ensure_can_withdraw(&CHARLIE, 26, WithdrawReasons::TRANSFER, 4).is_err());
+
+		MockBlockNumberProvider::set(14);
+
+		assert_ok!(Vesting::claim(Origin::signed(CHARLIE)));
+
+		assert_ok!(PalletBalances::ensure_can_withdraw(
+			&CHARLIE,
+			30,
+			WithdrawReasons::TRANSFER,
+			0
+		));
+	});
+}
+
+#[test]
+fn vested_transfer_works() {
+	ExtBuilder::build().execute_with(|| {
+		System::set_block_number(1);
+
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 1u32,
+			per_period: 100u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()));
+		assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule.clone()]);
+		System::assert_last_event(Event::Vesting(crate::Event::VestingScheduleAdded(ALICE, BOB, schedule)));
+	});
+}
+
+#[test]
+fn add_new_vesting_schedule_merges_with_current_locked_balance_and_until() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule));
+
+		MockBlockNumberProvider::set(12);
+
+		let another_schedule = VestingSchedule {
+			start: 10u64,
+			period: 13u64,
+			period_count: 1u32,
+			per_period: 7u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, another_schedule));
+
+		assert_eq!(
+			PalletBalances::locks(&BOB).get(0),
+			Some(&BalanceLock {
+				id: VESTING_LOCK_ID,
+				amount: 17u64,
+				reasons: Reasons::All,
+			})
+		);
+	});
+}
+
+#[test]
+fn cannot_use_fund_if_not_claimed() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 10u64,
+			period: 10u64,
+			period_count: 1u32,
+			per_period: 50u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule));
+		assert!(PalletBalances::ensure_can_withdraw(&BOB, 1, WithdrawReasons::TRANSFER, 49).is_err());
+	});
+}
+
+#[test]
+fn vested_transfer_fails_if_zero_period_or_count() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 1u64,
+			period: 0u64,
+			period_count: 1u32,
+			per_period: 100u64,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule),
+			Error::<Runtime>::ZeroVestingPeriod
+		);
+
+		let schedule = VestingSchedule {
+			start: 1u64,
+			period: 1u64,
+			period_count: 0u32,
+			per_period: 100u64,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule),
+			Error::<Runtime>::ZeroVestingPeriodCount
+		);
+	});
+}
+
+#[test]
+fn vested_transfer_fails_if_transfer_err() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 1u64,
+			period: 1u64,
+			period_count: 1u32,
+			per_period: 100u64,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(BOB), ALICE, schedule),
+			pallet_balances::Error::<Runtime, _>::InsufficientBalance,
+		);
+	});
+}
+
+#[test]
+fn vested_transfer_fails_if_overflow() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 1u64,
+			period: 1u64,
+			period_count: 2u32,
+			per_period: u64::MAX,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule),
+			ArithmeticError::Overflow,
+		);
+
+		let another_schedule = VestingSchedule {
+			start: u64::MAX,
+			period: 1u64,
+			period_count: 2u32,
+			per_period: 1u64,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(ALICE), BOB, another_schedule),
+			ArithmeticError::Overflow,
+		);
+	});
+}
+
+#[test]
+fn vested_transfer_fails_if_bad_origin() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 1u32,
+			per_period: 100u64,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(CHARLIE), BOB, schedule),
+			BadOrigin
+		);
+	});
+}
+
+#[test]
+fn claim_works() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule));
+
+		MockBlockNumberProvider::set(11);
+		// remain locked if not claimed
+		assert!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10).is_err());
+		// unlocked after claiming
+		assert_ok!(Vesting::claim(Origin::signed(BOB)));
+		assert!(VestingSchedules::<Runtime>::contains_key(BOB));
+		assert_ok!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10));
+		// more are still locked
+		assert!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 1).is_err());
+
+		MockBlockNumberProvider::set(21);
+		// claim more
+		assert_ok!(Vesting::claim(Origin::signed(BOB)));
+		assert!(!VestingSchedules::<Runtime>::contains_key(BOB));
+		assert_ok!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10));
+		// all used up
+		assert_eq!(PalletBalances::free_balance(BOB), 0);
+
+		// no locks anymore
+		assert_eq!(PalletBalances::locks(&BOB), vec![]);
+	});
+}
+
+#[test]
+fn claim_for_works() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule));
+
+		assert_ok!(Vesting::claim_for(Origin::signed(ALICE), BOB));
+
+		assert_eq!(
+			PalletBalances::locks(&BOB).get(0),
+			Some(&BalanceLock {
+				id: VESTING_LOCK_ID,
+				amount: 20u64,
+				reasons: Reasons::All,
+			})
+		);
+		assert!(VestingSchedules::<Runtime>::contains_key(&BOB));
+
+		MockBlockNumberProvider::set(21);
+
+		assert_ok!(Vesting::claim_for(Origin::signed(ALICE), BOB));
+
+		// no locks anymore
+		assert_eq!(PalletBalances::locks(&BOB), vec![]);
+		assert!(!VestingSchedules::<Runtime>::contains_key(&BOB));
+	});
+}
+
+#[test]
+fn update_vesting_schedules_works() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule));
+
+		let updated_schedule = VestingSchedule {
+			start: 0u64,
+			period: 20u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::update_vesting_schedules(
+			Origin::root(),
+			BOB,
+			vec![updated_schedule]
+		));
+
+		MockBlockNumberProvider::set(11);
+		assert_ok!(Vesting::claim(Origin::signed(BOB)));
+		assert!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 1).is_err());
+
+		MockBlockNumberProvider::set(21);
+		assert_ok!(Vesting::claim(Origin::signed(BOB)));
+		assert_ok!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10));
+
+		// empty vesting schedules cleanup the storage and unlock the fund
+		assert!(VestingSchedules::<Runtime>::contains_key(BOB));
+		assert_eq!(
+			PalletBalances::locks(&BOB).get(0),
+			Some(&BalanceLock {
+				id: VESTING_LOCK_ID,
+				amount: 10u64,
+				reasons: Reasons::All,
+			})
+		);
+		assert_ok!(Vesting::update_vesting_schedules(Origin::root(), BOB, vec![]));
+		assert!(!VestingSchedules::<Runtime>::contains_key(BOB));
+		assert_eq!(PalletBalances::locks(&BOB), vec![]);
+	});
+}
+
+#[test]
+fn update_vesting_schedules_fails_if_unexpected_existing_locks() {
+	ExtBuilder::build().execute_with(|| {
+		assert_ok!(PalletBalances::transfer(Origin::signed(ALICE), BOB, 1));
+		PalletBalances::set_lock(*b"prelocks", &BOB, 0u64, WithdrawReasons::all());
+	});
+}
+
+#[test]
+fn vested_transfer_check_for_min() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 1u64,
+			period: 1u64,
+			period_count: 1u32,
+			per_period: 3u64,
+		};
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(BOB), ALICE, schedule),
+			Error::<Runtime>::AmountLow
+		);
+	});
+}
+
+#[test]
+fn multiple_vesting_schedule_claim_works() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()));
+
+		let schedule2 = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 3u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule2.clone()));
+
+		assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule, schedule2.clone()]);
+
+		MockBlockNumberProvider::set(21);
+
+		assert_ok!(Vesting::claim(Origin::signed(BOB)));
+
+		assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule2]);
+
+		MockBlockNumberProvider::set(31);
+
+		assert_ok!(Vesting::claim(Origin::signed(BOB)));
+
+		assert!(!VestingSchedules::<Runtime>::contains_key(&BOB));
+
+		assert_eq!(PalletBalances::locks(&BOB), vec![]);
+	});
+}
+
+#[test]
+fn exceeding_maximum_schedules_should_fail() {
+	ExtBuilder::build().execute_with(|| {
+		let schedule = VestingSchedule {
+			start: 0u64,
+			period: 10u64,
+			period_count: 2u32,
+			per_period: 10u64,
+		};
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()));
+		assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()));
+		assert_noop!(
+			Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()),
+			Error::<Runtime>::MaxVestingSchedulesExceeded
+		);
+
+		let schedules = vec![schedule.clone(), schedule.clone(), schedule];
+
+		assert_noop!(
+			Vesting::update_vesting_schedules(Origin::root(), BOB, schedules),
+			Error::<Runtime>::MaxVestingSchedulesExceeded
+		);
+	});
+}
addedpallets/orml-vesting/src/weights.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/orml-vesting/src/weights.rs
@@ -0,0 +1,59 @@
+//! Autogenerated weights for orml_vesting
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
+//! DATE: 2021-05-04, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
+//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
+
+// Executed Command:
+// /Users/xiliangchen/projects/acala/target/release/acala
+// benchmark
+// --chain=dev
+// --steps=50
+// --repeat=20
+// --pallet=orml_vesting
+// --extrinsic=*
+// --execution=wasm
+// --wasm-execution=compiled
+// --heap-pages=4096
+// --output=./vesting/src/weights.rs
+// --template
+// ../templates/orml-weight-template.hbs
+
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(clippy::unnecessary_cast)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+/// Weight functions needed for orml_vesting.
+pub trait WeightInfo {
+	fn vested_transfer() -> Weight;
+	fn claim(i: u32, ) -> Weight;
+	fn update_vesting_schedules(i: u32, ) -> Weight;
+}
+
+/// Default weights.
+impl WeightInfo for () {
+	fn vested_transfer() -> Weight {
+		(69_000_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
+	}
+	fn claim(i: u32, ) -> Weight {
+		(31_747_000 as Weight)
+			// Standard Error: 4_000
+			.saturating_add((63_000 as Weight).saturating_mul(i as Weight))
+			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(2 as Weight))
+	}
+	fn update_vesting_schedules(i: u32, ) -> Weight {
+		(29_457_000 as Weight)
+			// Standard Error: 4_000
+			.saturating_add((117_000 as Weight).saturating_mul(i as Weight))
+			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
+	}
+}
modifiedruntime/Cargo.tomldiffbeforeafterboth
362default-features = false362default-features = false
363363
364[dependencies.orml-vesting]364[dependencies.orml-vesting]
365git = "https://github.com/open-web3-stack/open-runtime-module-library"365path = '../pallets/orml-vesting'
366version = "0.4.1-dev" 366version = "0.4.1-dev"
367default-features = false367default-features = false
368368