orml_vesting/
lib.rs

1//! # Vesting Module
2//!
3//! ## Overview
4//!
5//! Vesting module provides a means of scheduled balance lock on an account. It
6//! uses the *graded vesting* way, which unlocks a specific amount of balance
7//! every period of time, until all balance unlocked.
8//!
9//! ### Vesting Schedule
10//!
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 periods
14//! `period_count` reached. Note in vesting schedules, *time* is measured by
15//! block number. All `VestingSchedule`s under an account could be queried in
16//! chain state.
17//!
18//! ## Interface
19//!
20//! ### Dispatchable Functions
21//!
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 an
25//!   account, `root` origin required.
26
27#![cfg_attr(not(feature = "std"), no_std)]
28#![allow(clippy::unused_unit)]
29
30use frame_support::{
31	ensure,
32	pallet_prelude::*,
33	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, WithdrawReasons},
34	BoundedVec,
35};
36use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
37use parity_scale_codec::{HasCompact, MaxEncodedLen};
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	vec::Vec,
46};
47
48mod mock;
49mod tests;
50mod weights;
51
52pub use module::*;
53pub use weights::WeightInfo;
54
55pub const VESTING_LOCK_ID: LockIdentifier = *b"ormlvest";
56
57/// The vesting schedule.
58///
59/// Benefits would be granted gradually, `per_period` amount every `period`
60/// of blocks after `start`.
61#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo, DecodeWithMemTracking)]
62pub struct VestingSchedule<BlockNumber, Balance>
63where
64	Balance: MaxEncodedLen + HasCompact,
65{
66	/// Vesting starting block
67	pub start: BlockNumber,
68	/// Number of blocks between vest
69	pub period: BlockNumber,
70	/// Number of vest
71	pub period_count: u32,
72	/// Amount of tokens to release per vest
73	#[codec(compact)]
74	pub per_period: Balance,
75}
76
77impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + MaxEncodedLen + Copy>
78	VestingSchedule<BlockNumber, Balance>
79{
80	/// Returns the end of all periods, `None` if calculation overflows.
81	pub fn end(&self) -> Option<BlockNumber> {
82		// period * period_count + start
83		self.period
84			.checked_mul(&self.period_count.into())?
85			.checked_add(&self.start)
86	}
87
88	/// Returns all locked amount, `None` if calculation overflows.
89	pub fn total_amount(&self) -> Option<Balance> {
90		self.per_period.checked_mul(&self.period_count.into())
91	}
92
93	/// Returns locked amount for a given `time`.
94	///
95	/// Note this func assumes schedule is a valid one(non-zero period and
96	/// non-overflow total amount), and it should be guaranteed by callers.
97	pub fn locked_amount(&self, time: BlockNumber) -> Balance {
98		// full = (time - start) / period
99		// unrealized = period_count - full
100		// per_period * unrealized
101		let full = time
102			.saturating_sub(self.start)
103			.checked_div(&self.period)
104			.expect("ensured non-zero period; qed");
105		let unrealized = self.period_count.saturating_sub(full.unique_saturated_into());
106		self.per_period
107			.checked_mul(&unrealized.into())
108			.expect("ensured non-overflow total amount; qed")
109	}
110}
111
112#[frame_support::pallet]
113pub mod module {
114	use super::*;
115
116	pub(crate) type BalanceOf<T> =
117		<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
118	pub(crate) type VestingScheduleOf<T> = VestingSchedule<BlockNumberFor<T>, BalanceOf<T>>;
119	pub type ScheduledItem<T> = (
120		<T as frame_system::Config>::AccountId,
121		BlockNumberFor<T>,
122		BlockNumberFor<T>,
123		u32,
124		BalanceOf<T>,
125	);
126
127	#[pallet::config]
128	pub trait Config: frame_system::Config {
129		type Currency: LockableCurrency<Self::AccountId, Moment = BlockNumberFor<Self>>;
130
131		#[pallet::constant]
132		/// The minimum amount transferred to call `vested_transfer`.
133		type MinVestedTransfer: Get<BalanceOf<Self>>;
134
135		/// Required origin for vested transfer.
136		type VestedTransferOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
137
138		/// Weight information for extrinsics in this module.
139		type WeightInfo: WeightInfo;
140
141		/// The maximum vesting schedules
142		type MaxVestingSchedules: Get<u32>;
143
144		// The block number provider
145		type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
146	}
147
148	#[pallet::error]
149	pub enum Error<T> {
150		/// Vesting period is zero
151		ZeroVestingPeriod,
152		/// Number of vests is zero
153		ZeroVestingPeriodCount,
154		/// Insufficient amount of balance to lock
155		InsufficientBalanceToLock,
156		/// This account have too many vesting schedules
157		TooManyVestingSchedules,
158		/// The vested transfer amount is too low
159		AmountLow,
160		/// Failed because the maximum vesting schedules was exceeded
161		MaxVestingSchedulesExceeded,
162	}
163
164	#[pallet::event]
165	#[pallet::generate_deposit(fn deposit_event)]
166	pub enum Event<T: Config> {
167		/// Added new vesting schedule.
168		VestingScheduleAdded {
169			from: T::AccountId,
170			to: T::AccountId,
171			vesting_schedule: VestingScheduleOf<T>,
172		},
173		/// Claimed vesting.
174		Claimed { who: T::AccountId, amount: BalanceOf<T> },
175		/// Updated vesting schedules.
176		VestingSchedulesUpdated { who: T::AccountId },
177	}
178
179	/// Vesting schedules of an account.
180	///
181	/// VestingSchedules: map AccountId => Vec<VestingSchedule>
182	#[pallet::storage]
183	#[pallet::getter(fn vesting_schedules)]
184	pub type VestingSchedules<T: Config> = StorageMap<
185		_,
186		Blake2_128Concat,
187		T::AccountId,
188		BoundedVec<VestingScheduleOf<T>, T::MaxVestingSchedules>,
189		ValueQuery,
190	>;
191
192	#[pallet::genesis_config]
193	pub struct GenesisConfig<T: Config> {
194		pub vesting: Vec<ScheduledItem<T>>,
195	}
196
197	impl<T: Config> Default for GenesisConfig<T> {
198		fn default() -> Self {
199			GenesisConfig {
200				vesting: Default::default(),
201			}
202		}
203	}
204
205	#[pallet::genesis_build]
206	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
207		fn build(&self) {
208			self.vesting
209				.iter()
210				.for_each(|(who, start, period, period_count, per_period)| {
211					let mut bounded_schedules = VestingSchedules::<T>::get(who);
212					bounded_schedules
213						.try_push(VestingSchedule {
214							start: *start,
215							period: *period,
216							period_count: *period_count,
217							per_period: *per_period,
218						})
219						.expect("Max vesting schedules exceeded");
220					let total_amount = bounded_schedules
221						.iter()
222						.try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(Zero::zero(), |acc_amount, schedule| {
223							let amount = ensure_valid_vesting_schedule::<T>(schedule)?;
224							acc_amount
225								.checked_add(&amount)
226								.ok_or_else(|| ArithmeticError::Overflow.into())
227						})
228						.expect("Invalid vesting schedule");
229
230					assert!(
231						T::Currency::free_balance(who) >= total_amount,
232						"Account do not have enough balance"
233					);
234
235					T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());
236					VestingSchedules::<T>::insert(who, bounded_schedules);
237				});
238		}
239	}
240
241	#[pallet::pallet]
242	pub struct Pallet<T>(_);
243
244	#[pallet::hooks]
245	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
246
247	#[pallet::call]
248	impl<T: Config> Pallet<T> {
249		#[pallet::call_index(0)]
250		#[pallet::weight(T::WeightInfo::claim(<T as Config>::MaxVestingSchedules::get() / 2))]
251		pub fn claim(origin: OriginFor<T>) -> DispatchResult {
252			let who = ensure_signed(origin)?;
253			let locked_amount = Self::do_claim(&who);
254
255			Self::deposit_event(Event::Claimed {
256				who,
257				amount: locked_amount,
258			});
259			Ok(())
260		}
261
262		#[pallet::call_index(1)]
263		#[pallet::weight(T::WeightInfo::vested_transfer())]
264		pub fn vested_transfer(
265			origin: OriginFor<T>,
266			dest: <T::Lookup as StaticLookup>::Source,
267			schedule: VestingScheduleOf<T>,
268		) -> DispatchResult {
269			let from = T::VestedTransferOrigin::ensure_origin(origin)?;
270			let to = T::Lookup::lookup(dest)?;
271
272			if to == from {
273				ensure!(
274					T::Currency::free_balance(&from) >= schedule.total_amount().ok_or(ArithmeticError::Overflow)?,
275					Error::<T>::InsufficientBalanceToLock,
276				);
277			}
278
279			Self::do_vested_transfer(&from, &to, schedule.clone())?;
280
281			Self::deposit_event(Event::VestingScheduleAdded {
282				from,
283				to,
284				vesting_schedule: schedule,
285			});
286			Ok(())
287		}
288
289		#[pallet::call_index(2)]
290		#[pallet::weight(T::WeightInfo::update_vesting_schedules(vesting_schedules.len() as u32))]
291		pub fn update_vesting_schedules(
292			origin: OriginFor<T>,
293			who: <T::Lookup as StaticLookup>::Source,
294			vesting_schedules: Vec<VestingScheduleOf<T>>,
295		) -> DispatchResult {
296			ensure_root(origin)?;
297
298			let account = T::Lookup::lookup(who)?;
299			Self::do_update_vesting_schedules(&account, vesting_schedules)?;
300
301			Self::deposit_event(Event::VestingSchedulesUpdated { who: account });
302			Ok(())
303		}
304
305		#[pallet::call_index(3)]
306		#[pallet::weight(T::WeightInfo::claim(<T as Config>::MaxVestingSchedules::get() / 2))]
307		pub fn claim_for(origin: OriginFor<T>, dest: <T::Lookup as StaticLookup>::Source) -> DispatchResult {
308			let _ = ensure_signed(origin)?;
309			let who = T::Lookup::lookup(dest)?;
310			let locked_amount = Self::do_claim(&who);
311
312			Self::deposit_event(Event::Claimed {
313				who,
314				amount: locked_amount,
315			});
316			Ok(())
317		}
318	}
319}
320
321impl<T: Config> Pallet<T> {
322	fn do_claim(who: &T::AccountId) -> BalanceOf<T> {
323		let locked = Self::locked_balance(who);
324		if locked.is_zero() {
325			// cleanup the storage and unlock the fund
326			<VestingSchedules<T>>::remove(who);
327			T::Currency::remove_lock(VESTING_LOCK_ID, who);
328		} else {
329			T::Currency::set_lock(VESTING_LOCK_ID, who, locked, WithdrawReasons::all());
330		}
331		locked
332	}
333
334	/// Returns locked balance based on current block number.
335	fn locked_balance(who: &T::AccountId) -> BalanceOf<T> {
336		let now = T::BlockNumberProvider::current_block_number();
337		<VestingSchedules<T>>::mutate_exists(who, |maybe_schedules| {
338			let total = if let Some(schedules) = maybe_schedules.as_mut() {
339				let mut total: BalanceOf<T> = Zero::zero();
340				schedules.retain(|s| {
341					let amount = s.locked_amount(now);
342					total = total.saturating_add(amount);
343					!amount.is_zero()
344				});
345				total
346			} else {
347				Zero::zero()
348			};
349			if total.is_zero() {
350				*maybe_schedules = None;
351			}
352			total
353		})
354	}
355
356	fn do_vested_transfer(from: &T::AccountId, to: &T::AccountId, schedule: VestingScheduleOf<T>) -> DispatchResult {
357		let schedule_amount = ensure_valid_vesting_schedule::<T>(&schedule)?;
358
359		let total_amount = Self::locked_balance(to)
360			.checked_add(&schedule_amount)
361			.ok_or(ArithmeticError::Overflow)?;
362
363		T::Currency::transfer(from, to, schedule_amount, ExistenceRequirement::AllowDeath)?;
364		T::Currency::set_lock(VESTING_LOCK_ID, to, total_amount, WithdrawReasons::all());
365		<VestingSchedules<T>>::try_append(to, schedule).map_err(|_| Error::<T>::MaxVestingSchedulesExceeded)?;
366		Ok(())
367	}
368
369	fn do_update_vesting_schedules(who: &T::AccountId, schedules: Vec<VestingScheduleOf<T>>) -> DispatchResult {
370		let bounded_schedules: BoundedVec<VestingScheduleOf<T>, T::MaxVestingSchedules> = schedules
371			.try_into()
372			.map_err(|_| Error::<T>::MaxVestingSchedulesExceeded)?;
373
374		// empty vesting schedules cleanup the storage and unlock the fund
375		if bounded_schedules.len().is_zero() {
376			<VestingSchedules<T>>::remove(who);
377			T::Currency::remove_lock(VESTING_LOCK_ID, who);
378			return Ok(());
379		}
380
381		let total_amount = bounded_schedules
382			.iter()
383			.try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(Zero::zero(), |acc_amount, schedule| {
384				let amount = ensure_valid_vesting_schedule::<T>(schedule)?;
385				acc_amount
386					.checked_add(&amount)
387					.ok_or_else(|| ArithmeticError::Overflow.into())
388			})?;
389		ensure!(
390			T::Currency::free_balance(who) >= total_amount,
391			Error::<T>::InsufficientBalanceToLock,
392		);
393
394		T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());
395		<VestingSchedules<T>>::insert(who, bounded_schedules);
396
397		Ok(())
398	}
399}
400
401/// Returns `Ok(total_total)` if valid schedule, or error.
402fn ensure_valid_vesting_schedule<T: Config>(schedule: &VestingScheduleOf<T>) -> Result<BalanceOf<T>, DispatchError> {
403	ensure!(!schedule.period.is_zero(), Error::<T>::ZeroVestingPeriod);
404	ensure!(!schedule.period_count.is_zero(), Error::<T>::ZeroVestingPeriodCount);
405	ensure!(schedule.end().is_some(), ArithmeticError::Overflow);
406
407	let total_total = schedule.total_amount().ok_or(ArithmeticError::Overflow)?;
408
409	ensure!(total_total >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
410
411	Ok(total_total)
412}