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

difftreelog

source

pallets/orml-vesting/src/lib.rs12.0 KiBsourcehistory
1//! # Vesting Module2//!3//! ## Overview4//!5//! Vesting module provides a means of scheduled balance lock on an account. It6//! uses the *graded vesting* way, which unlocks a specific amount of balance7//! every period of time, until all balance unlocked.8//!9//! ### Vesting Schedule10//!11//! The schedule of a vesting is described by data structure `VestingSchedule`:12//! from the block number of `start`, for every `period` amount of blocks,13//! `per_period` amount of balance would unlocked, until number of periods14//! `period_count` reached. Note in vesting schedules, *time* is measured by15//! block number. All `VestingSchedule`s under an account could be queried in16//! chain state.17//!18//! ## Interface19//!20//! ### Dispatchable Functions21//!22//! - `vested_transfer` - Add a new vesting schedule for an account.23//! - `claim` - Claim unlocked balances.24//! - `update_vesting_schedules` - Update all vesting schedules under an25//!   account, `root` origin required.2627#![cfg_attr(not(feature = "std"), no_std)]28#![allow(clippy::unused_unit)]2930use codec::{HasCompact, MaxEncodedLen};31use frame_support::{32	ensure,33	pallet_prelude::*,34	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, WithdrawReasons},35	transactional, BoundedVec,36};37use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};38use scale_info::TypeInfo;39use sp_runtime::{40	traits::{AtLeast32Bit, BlockNumberProvider, CheckedAdd, Saturating, StaticLookup, Zero},41	ArithmeticError, DispatchResult, RuntimeDebug,42};43use sp_std::{44	cmp::{Eq, PartialEq},45	convert::TryInto,46	vec::Vec,47};4849mod mock;50mod tests;51mod weights;5253pub use module::*;54pub use weights::WeightInfo;5556pub const VESTING_LOCK_ID: LockIdentifier = *b"ormlvest";5758/// The vesting schedule.59///60/// Benefits would be granted gradually, `per_period` amount every `period`61/// of blocks after `start`.62#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]63pub struct VestingSchedule<BlockNumber, Balance: HasCompact> {64	/// Vesting starting block65	pub start: BlockNumber,66	/// Number of blocks between vest67	pub period: BlockNumber,68	/// Number of vest69	pub period_count: u32,70	/// Amount of tokens to release per vest71	#[codec(compact)]72	pub per_period: Balance,73}7475impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + Copy> VestingSchedule<BlockNumber, Balance> {76	/// Returns the end of all periods, `None` if calculation overflows.77	pub fn end(&self) -> Option<BlockNumber> {78		// period * period_count + start79		self.period80			.checked_mul(&self.period_count.into())?81			.checked_add(&self.start)82	}8384	/// Returns all locked amount, `None` if calculation overflows.85	pub fn total_amount(&self) -> Option<Balance> {86		self.per_period.checked_mul(&self.period_count.into())87	}8889	/// Returns locked amount for a given `time`.90	///91	/// Note this func assumes schedule is a valid one(non-zero period and92	/// non-overflow total amount), and it should be guaranteed by callers.93	pub fn locked_amount(&self, time: BlockNumber) -> Balance {94		// full = (time - start) / period95		// unrealized = period_count - full96		// per_period * unrealized97		let full = time98			.saturating_sub(self.start)99			.checked_div(&self.period)100			.expect("ensured non-zero period; qed");101		let unrealized = self.period_count.saturating_sub(full.unique_saturated_into());102		self.per_period103			.checked_mul(&unrealized.into())104			.expect("ensured non-overflow total amount; qed")105	}106}107108#[frame_support::pallet]109pub mod module {110	use super::*;111112	pub(crate) type BalanceOf<T> =113		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;114	pub(crate) type VestingScheduleOf<T> = VestingSchedule<<T as frame_system::Config>::BlockNumber, BalanceOf<T>>;115	pub type ScheduledItem<T> = (116		<T as frame_system::Config>::AccountId,117		<T as frame_system::Config>::BlockNumber,118		<T as frame_system::Config>::BlockNumber,119		u32,120		BalanceOf<T>,121	);122123	#[pallet::config]124	pub trait Config: frame_system::Config {125		type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;126127		type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;128129		#[pallet::constant]130		/// The minimum amount transferred to call `vested_transfer`.131		type MinVestedTransfer: Get<BalanceOf<Self>>;132133		/// Required origin for vested transfer.134		type VestedTransferOrigin: EnsureOrigin<Self::Origin, Success = Self::AccountId>;135136		/// Weight information for extrinsics in this module.137		type WeightInfo: WeightInfo;138139		/// The maximum vesting schedules140		type MaxVestingSchedules: Get<u32>;141142		// The block number provider143		type BlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;144	}145146	#[pallet::error]147	pub enum Error<T> {148		/// Vesting period is zero149		ZeroVestingPeriod,150		/// Number of vests is zero151		ZeroVestingPeriodCount,152		/// Insufficient amount of balance to lock153		InsufficientBalanceToLock,154		/// This account have too many vesting schedules155		TooManyVestingSchedules,156		/// The vested transfer amount is too low157		AmountLow,158		/// Failed because the maximum vesting schedules was exceeded159		MaxVestingSchedulesExceeded,160	}161162	#[pallet::event]163	#[pallet::generate_deposit(fn deposit_event)]164	pub enum Event<T: Config> {165		/// Added new vesting schedule. \[from, to, vesting_schedule\]166		VestingScheduleAdded(T::AccountId, T::AccountId, VestingScheduleOf<T>),167		/// Claimed vesting. \[who, locked_amount\]168		Claimed(T::AccountId, BalanceOf<T>),169		/// Updated vesting schedules. \[who\]170		VestingSchedulesUpdated(T::AccountId),171	}172173	/// Vesting schedules of an account.174	///175	/// VestingSchedules: map AccountId => Vec<VestingSchedule>176	#[pallet::storage]177	#[pallet::getter(fn vesting_schedules)]178	pub type VestingSchedules<T: Config> = StorageMap<179		_,180		Blake2_128Concat,181		T::AccountId,182		BoundedVec<VestingScheduleOf<T>, T::MaxVestingSchedules>,183		ValueQuery,184	>;185186	#[pallet::genesis_config]187	pub struct GenesisConfig<T: Config> {188		pub vesting: Vec<ScheduledItem<T>>,189	}190191	#[cfg(feature = "std")]192	impl<T: Config> Default for GenesisConfig<T> {193		fn default() -> Self {194			GenesisConfig { vesting: vec![] }195		}196	}197198	#[pallet::genesis_build]199	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {200		fn build(&self) {201			self.vesting202				.iter()203				.for_each(|(who, start, period, period_count, per_period)| {204					let mut bounded_schedules = VestingSchedules::<T>::get(who);205					bounded_schedules206						.try_push(VestingSchedule {207							start: *start,208							period: *period,209							period_count: *period_count,210							per_period: *per_period,211						})212						.expect("Max vesting schedules exceeded");213					let total_amount = bounded_schedules214						.iter()215						.try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(Zero::zero(), |acc_amount, schedule| {216							let amount = ensure_valid_vesting_schedule::<T>(schedule)?;217							Ok(acc_amount + amount)218						})219						.expect("Invalid vesting schedule");220221					assert!(222						T::Currency::free_balance(who) >= total_amount,223						"Account do not have enough balance"224					);225226					T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());227					VestingSchedules::<T>::insert(who, bounded_schedules);228				});229		}230	}231232	#[pallet::pallet]233	pub struct Pallet<T>(_);234235	#[pallet::hooks]236	impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {}237238	#[pallet::call]239	impl<T: Config> Pallet<T> {240		#[pallet::weight(T::WeightInfo::claim((<T as Config>::MaxVestingSchedules::get() / 2) as u32))]241		pub fn claim(origin: OriginFor<T>) -> DispatchResult {242			let who = ensure_signed(origin)?;243			let locked_amount = Self::do_claim(&who);244245			Self::deposit_event(Event::Claimed(who, locked_amount));246			Ok(())247		}248249		#[pallet::weight(T::WeightInfo::vested_transfer())]250		pub fn vested_transfer(251			origin: OriginFor<T>,252			dest: <T::Lookup as StaticLookup>::Source,253			schedule: VestingScheduleOf<T>,254		) -> DispatchResult {255			let from = T::VestedTransferOrigin::ensure_origin(origin)?;256			let to = T::Lookup::lookup(dest)?;257			Self::do_vested_transfer(&from, &to, schedule.clone())?;258259			Self::deposit_event(Event::VestingScheduleAdded(from, to, schedule));260			Ok(())261		}262263		#[pallet::weight(T::WeightInfo::update_vesting_schedules(vesting_schedules.len() as u32))]264		pub fn update_vesting_schedules(265			origin: OriginFor<T>,266			who: <T::Lookup as StaticLookup>::Source,267			vesting_schedules: Vec<VestingScheduleOf<T>>,268		) -> DispatchResult {269			ensure_root(origin)?;270271			let account = T::Lookup::lookup(who)?;272			Self::do_update_vesting_schedules(&account, vesting_schedules)?;273274			Self::deposit_event(Event::VestingSchedulesUpdated(account));275			Ok(())276		}277278		#[pallet::weight(T::WeightInfo::claim((<T as Config>::MaxVestingSchedules::get() / 2) as u32))]279		pub fn claim_for(origin: OriginFor<T>, dest: <T::Lookup as StaticLookup>::Source) -> DispatchResult {280			let _ = ensure_signed(origin)?;281			let who = T::Lookup::lookup(dest)?;282			let locked_amount = Self::do_claim(&who);283284			Self::deposit_event(Event::Claimed(who, locked_amount));285			Ok(())286		}287	}288}289290impl<T: Config> Pallet<T> {291	fn do_claim(who: &T::AccountId) -> BalanceOf<T> {292		let locked = Self::locked_balance(who);293		if locked.is_zero() {294			// cleanup the storage and unlock the fund295			<VestingSchedules<T>>::remove(who);296			T::Currency::remove_lock(VESTING_LOCK_ID, who);297		} else {298			T::Currency::set_lock(VESTING_LOCK_ID, who, locked, WithdrawReasons::all());299		}300		locked301	}302303	/// Returns locked balance based on current block number.304	fn locked_balance(who: &T::AccountId) -> BalanceOf<T> {305		let now = T::BlockNumberProvider::current_block_number();306		<VestingSchedules<T>>::mutate_exists(who, |maybe_schedules| {307			let total = if let Some(schedules) = maybe_schedules.as_mut() {308				let mut total: BalanceOf<T> = Zero::zero();309				schedules.retain(|s| {310					let amount = s.locked_amount(now);311					total = total.saturating_add(amount);312					!amount.is_zero()313				});314				total315			} else {316				Zero::zero()317			};318			if total.is_zero() {319				*maybe_schedules = None;320			}321			total322		})323	}324325	#[transactional]326	fn do_vested_transfer(from: &T::AccountId, to: &T::AccountId, schedule: VestingScheduleOf<T>) -> DispatchResult {327		let schedule_amount = ensure_valid_vesting_schedule::<T>(&schedule)?;328329		let total_amount = Self::locked_balance(to)330			.checked_add(&schedule_amount)331			.ok_or(ArithmeticError::Overflow)?;332333		T::Currency::transfer(from, to, schedule_amount, ExistenceRequirement::AllowDeath)?;334		T::Currency::set_lock(VESTING_LOCK_ID, to, total_amount, WithdrawReasons::all());335		<VestingSchedules<T>>::try_append(to, schedule).map_err(|_| Error::<T>::MaxVestingSchedulesExceeded)?;336		Ok(())337	}338339	fn do_update_vesting_schedules(who: &T::AccountId, schedules: Vec<VestingScheduleOf<T>>) -> DispatchResult {340		let bounded_schedules: BoundedVec<VestingScheduleOf<T>, T::MaxVestingSchedules> = schedules341			.try_into()342			.map_err(|_| Error::<T>::MaxVestingSchedulesExceeded)?;343344		// empty vesting schedules cleanup the storage and unlock the fund345		if bounded_schedules.len().is_zero() {346			<VestingSchedules<T>>::remove(who);347			T::Currency::remove_lock(VESTING_LOCK_ID, who);348			return Ok(());349		}350351		let total_amount = bounded_schedules352			.iter()353			.try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(Zero::zero(), |acc_amount, schedule| {354				let amount = ensure_valid_vesting_schedule::<T>(schedule)?;355				Ok(acc_amount + amount)356			})?;357		ensure!(358			T::Currency::free_balance(who) >= total_amount,359			Error::<T>::InsufficientBalanceToLock,360		);361362		T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());363		<VestingSchedules<T>>::insert(who, bounded_schedules);364365		Ok(())366	}367}368369/// Returns `Ok(total_total)` if valid schedule, or error.370fn ensure_valid_vesting_schedule<T: Config>(schedule: &VestingScheduleOf<T>) -> Result<BalanceOf<T>, DispatchError> {371	ensure!(!schedule.period.is_zero(), Error::<T>::ZeroVestingPeriod);372	ensure!(!schedule.period_count.is_zero(), Error::<T>::ZeroVestingPeriodCount);373	ensure!(schedule.end().is_some(), ArithmeticError::Overflow);374375	let total_total = schedule.total_amount().ok_or(ArithmeticError::Overflow)?;376377	ensure!(total_total >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);378379	Ok(total_total)380}