Skip to content
Snippets Groups Projects
lib.rs 10.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • //! # 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
    //!
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    //! - `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.
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    #![cfg_attr(not(feature = "std"), no_std)]
    
    use codec::{Decode, Encode, HasCompact};
    use frame_support::{
    	decl_error, decl_event, decl_module, decl_storage, ensure,
    
    	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, WithdrawReasons},
    
    	transactional,
    
    	weights::Weight,
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    };
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    use frame_system::{ensure_root, ensure_signed};
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    use sp_runtime::{
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    	traits::{AtLeast32Bit, CheckedAdd, Saturating, StaticLookup, Zero},
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	DispatchResult, RuntimeDebug,
    };
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    use sp_std::{
    	cmp::{Eq, PartialEq},
    	vec::Vec,
    };
    
    mod default_weight;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    mod mock;
    mod tests;
    
    
    pub trait WeightInfo {
    	fn vested_transfer() -> Weight;
    
    zjb0807's avatar
    zjb0807 committed
    	fn claim(i: u32) -> Weight;
    	fn update_vesting_schedules(i: u32) -> Weight;
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    /// The maximum number of vesting schedules an account can have.
    pub const MAX_VESTINGS: usize = 20;
    
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    /// The vesting schedule.
    ///
    
    /// Benefits would be granted gradually, `per_period` amount every `period` of
    /// blocks after `start`.
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    #[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug)]
    pub struct VestingSchedule<BlockNumber, Balance: HasCompact> {
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    	/// Vesting starting block
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	pub start: BlockNumber,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    	/// Number of blocks between vest
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	pub period: BlockNumber,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    	/// Number of vest
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	pub period_count: u32,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    	/// Amount of tokens to release per vest
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	#[codec(compact)]
    	pub per_period: Balance,
    }
    
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + Copy> VestingSchedule<BlockNumber, Balance> {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	/// Returns the end of all periods, `None` if calculation overflows.
    	pub fn end(&self) -> Option<BlockNumber> {
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		// period * period_count + start
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		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.
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	pub fn locked_amount(&self, time: BlockNumber) -> Balance {
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		// full = (time - start) / period
    		// unrealized = period_count - full
    		// per_period * unrealized
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		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")
    	}
    }
    
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    pub type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
    pub type VestingScheduleOf<T> = VestingSchedule<<T as frame_system::Config>::BlockNumber, BalanceOf<T>>;
    
    Keith Yeung's avatar
    Keith Yeung committed
    pub type ScheduledItem<T> = (
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	<T as frame_system::Config>::AccountId,
    	<T as frame_system::Config>::BlockNumber,
    	<T as frame_system::Config>::BlockNumber,
    
    Keith Yeung's avatar
    Keith Yeung committed
    	u32,
    	BalanceOf<T>,
    );
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    pub trait Config: frame_system::Config {
    	type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    
    	/// 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;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    }
    
    decl_storage! {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	trait Store for Module<T: Config> as Vesting {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		/// Vesting schedules of an account.
    		pub VestingSchedules get(fn vesting_schedules) build(|config: &GenesisConfig<T>| {
    			config.vesting.iter()
    
    				.map(|&(ref who, start, period, period_count, per_period)| {
    					let total = per_period * Into::<BalanceOf<T>>::into(period_count);
    					assert!(T::Currency::free_balance(who) >= total, "Account do not have enough balance");
    					T::Currency::set_lock(VESTING_LOCK_ID, who, total, WithdrawReasons::all());
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    					(who.clone(), vec![VestingSchedule {start, period, period_count, per_period}])
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    				.collect::<Vec<_>>()
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		}): map hasher(blake2_128_concat) T::AccountId => Vec<VestingScheduleOf<T>>;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	}
    
    	add_extra_genesis {
    
    Keith Yeung's avatar
    Keith Yeung committed
    		config(vesting): Vec<ScheduledItem<T>>;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	}
    }
    
    decl_event!(
    	pub enum Event<T> where
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		<T as frame_system::Config>::AccountId,
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		Balance = BalanceOf<T>,
    		VestingSchedule = VestingScheduleOf<T>
    	{
    
    		/// Added new vesting schedule. [from, to, vesting_schedule]
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		VestingScheduleAdded(AccountId, AccountId, VestingSchedule),
    
    		/// Claimed vesting. [who, locked_amount]
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		Claimed(AccountId, Balance),
    
    		/// Updated vesting schedules. [who]
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		VestingSchedulesUpdated(AccountId),
    	}
    );
    
    decl_error! {
    	/// Error for vesting module.
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	pub enum Error for Module<T: Config> {
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// Vesting period is zero
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		ZeroVestingPeriod,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// Number of vests is zero
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		ZeroVestingPeriodCount,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// Arithmetic calculation overflow
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		NumOverflow,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// Insufficient amount of balance to lock
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		InsufficientBalanceToLock,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// This account have too many vesting schedules
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		TooManyVestingSchedules,
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// The vested transfer amount is too low
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		AmountLow,
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	}
    }
    
    decl_module! {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	pub struct Module<T: Config> for enum Call where origin: T::Origin {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		type Error = Error<T>;
    
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		/// The minimum amount to be transferred to create a new vesting schedule.
    		const MinVestedTransfer: BalanceOf<T> = T::MinVestedTransfer::get();
    
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		fn deposit_event() = default;
    
    
    		/// # <weight>
    		/// - Preconditions:
    		/// 	- T::Currency is orml_currencies
    		/// - Complexity: `O(1)`
    
    		/// - Db reads: 3
    		/// - Db writes: 3
    
    		/// Base Weight: 65.23 µs
    
    zjb0807's avatar
    zjb0807 committed
    		// can not get VestingSchedule count from `who`, so use `MAX_VESTINGS / 2`
    		#[weight = T::WeightInfo::claim((MAX_VESTINGS / 2) as u32)]
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		pub fn claim(origin) {
    			let who = ensure_signed(origin)?;
    			let locked_amount = Self::do_claim(&who);
    
    			Self::deposit_event(RawEvent::Claimed(who, locked_amount));
    		}
    
    
    		/// # <weight>
    		/// - Preconditions:
    		/// 	- T::Currency is orml_currencies
    		/// - Complexity: `O(1)`
    
    		/// - Db reads: 4
    		/// - Db writes: 4
    
    		/// Base Weight: 150.4 µs
    
    		#[weight = T::WeightInfo::vested_transfer()]
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		pub fn vested_transfer(
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    			origin,
    			dest: <T::Lookup as StaticLookup>::Source,
    			schedule: VestingScheduleOf<T>,
    		) {
    
    			let from = T::VestedTransferOrigin::ensure_origin(origin)?;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    			let to = T::Lookup::lookup(dest)?;
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    			Self::do_vested_transfer(&from, &to, schedule.clone())?;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    
    			Self::deposit_event(RawEvent::VestingScheduleAdded(from, to, schedule));
    		}
    
    
    		/// # <weight>
    		/// - Preconditions:
    		/// 	- T::Currency is orml_currencies
    		/// - Complexity: `O(1)`
    
    		/// - Db reads: 2
    		/// - Db writes: 3
    
    		/// Base Weight: 61.87 µs
    
    zjb0807's avatar
    zjb0807 committed
    		#[weight = T::WeightInfo::update_vesting_schedules(vesting_schedules.len() as u32)]
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		pub fn update_vesting_schedules(
    			origin,
    			who: <T::Lookup as StaticLookup>::Source,
    			vesting_schedules: Vec<VestingScheduleOf<T>>
    		) {
    			ensure_root(origin)?;
    
    			let account = T::Lookup::lookup(who)?;
    			Self::do_update_vesting_schedules(&account, vesting_schedules)?;
    
    			Self::deposit_event(RawEvent::VestingSchedulesUpdated(account));
    		}
    	}
    }
    
    const VESTING_LOCK_ID: LockIdentifier = *b"ormlvest";
    
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    impl<T: Config> Module<T> {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    	fn do_claim(who: &T::AccountId) -> BalanceOf<T> {
    
    		let locked = Self::locked_balance(who);
    		if locked.is_zero() {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    			T::Currency::remove_lock(VESTING_LOCK_ID, who);
    
    		} else {
    			T::Currency::set_lock(VESTING_LOCK_ID, who, locked, WithdrawReasons::all());
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		}
    
    	/// Returns locked balance based on current block number.
    	fn locked_balance(who: &T::AccountId) -> BalanceOf<T> {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		let now = <frame_system::Module<T>>::block_number();
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		<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 = Self::ensure_valid_vesting_schedule(&schedule)?;
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    
    		ensure!(
    			<VestingSchedules<T>>::decode_len(to).unwrap_or(0) < MAX_VESTINGS,
    			Error::<T>::TooManyVestingSchedules
    		);
    
    
    		let total_amount = Self::locked_balance(to)
    
    Keith Yeung's avatar
    Keith Yeung committed
    			.checked_add(&schedule_amount)
    
    			.ok_or(Error::<T>::NumOverflow)?;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    
    		T::Currency::transfer(from, to, schedule_amount, ExistenceRequirement::AllowDeath)?;
    
    		T::Currency::set_lock(VESTING_LOCK_ID, to, total_amount, WithdrawReasons::all());
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		<VestingSchedules<T>>::append(to, schedule);
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		Ok(())
    	}
    
    	fn do_update_vesting_schedules(who: &T::AccountId, schedules: Vec<VestingScheduleOf<T>>) -> DispatchResult {
    
    		let total_amount = schedules.iter().try_fold::<_, _, Result<BalanceOf<T>, Error<T>>>(
    			Zero::zero(),
    			|acc_amount, schedule| {
    				let amount = Self::ensure_valid_vesting_schedule(schedule)?;
    				Ok(acc_amount + amount)
    			},
    		)?;
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		ensure!(
    			T::Currency::free_balance(who) >= total_amount,
    			Error::<T>::InsufficientBalanceToLock,
    		);
    
    
    		T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all());
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		<VestingSchedules<T>>::insert(who, schedules);
    
    		Ok(())
    	}
    
    
    	/// Returns `Ok(amount)` if valid schedule, or error.
    	fn ensure_valid_vesting_schedule(schedule: &VestingScheduleOf<T>) -> Result<BalanceOf<T>, Error<T>> {
    
    Shaopeng Wang's avatar
    Shaopeng Wang committed
    		ensure!(!schedule.period.is_zero(), Error::<T>::ZeroVestingPeriod);
    		ensure!(!schedule.period_count.is_zero(), Error::<T>::ZeroVestingPeriodCount);
    
    		ensure!(schedule.end().is_some(), Error::<T>::NumOverflow);
    
    Xiliang Chen's avatar
    Xiliang Chen committed
    		let total = schedule.total_amount().ok_or(Error::<T>::NumOverflow)?;
    
    		ensure!(total >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
    
    		Ok(total)