#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::unused_unit)]
use frame_support::{
ensure,
pallet_prelude::*,
traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, WithdrawReasons},
BoundedVec,
};
use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
use parity_scale_codec::{HasCompact, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{AtLeast32Bit, BlockNumberProvider, CheckedAdd, Saturating, StaticLookup, Zero},
ArithmeticError, DispatchResult, RuntimeDebug,
};
use sp_std::{
cmp::{Eq, PartialEq},
vec::Vec,
};
mod mock;
mod tests;
mod weights;
pub use module::*;
pub use weights::WeightInfo;
pub const VESTING_LOCK_ID: LockIdentifier = *b"ormlvest";
#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub struct VestingSchedule<BlockNumber, Balance: MaxEncodedLen + HasCompact> {
pub start: BlockNumber,
pub period: BlockNumber,
pub period_count: u32,
#[codec(compact)]
pub per_period: Balance,
}
impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + MaxEncodedLen + Copy>
VestingSchedule<BlockNumber, Balance>
{
pub fn end(&self) -> Option<BlockNumber> {
self.period
.checked_mul(&self.period_count.into())?
.checked_add(&self.start)
}
pub fn total_amount(&self) -> Option<Balance> {
self.per_period.checked_mul(&self.period_count.into())
}
pub fn locked_amount(&self, time: BlockNumber) -> Balance {
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<BlockNumberFor<T>, BalanceOf<T>>;
pub type ScheduledItem<T> = (
<T as frame_system::Config>::AccountId,
BlockNumberFor<T>,
BlockNumberFor<T>,
u32,
BalanceOf<T>,
);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Currency: LockableCurrency<Self::AccountId, Moment = BlockNumberFor<Self>>;
#[pallet::constant]
type MinVestedTransfer: Get<BalanceOf<Self>>;
type VestedTransferOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
type WeightInfo: WeightInfo;
type MaxVestingSchedules: Get<u32>;
type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
}
#[pallet::error]
pub enum Error<T> {
ZeroVestingPeriod,
ZeroVestingPeriodCount,
InsufficientBalanceToLock,
TooManyVestingSchedules,
AmountLow,
MaxVestingSchedulesExceeded,
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
VestingScheduleAdded {
from: T::AccountId,
to: T::AccountId,
vesting_schedule: VestingScheduleOf<T>,
},
Claimed { who: T::AccountId, amount: BalanceOf<T> },
VestingSchedulesUpdated { who: T::AccountId },
}
#[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>>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig {
vesting: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig 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)?;
acc_amount
.checked_add(&amount)
.ok_or_else(|| ArithmeticError::Overflow.into())
})
.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<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::claim(<T as Config>::MaxVestingSchedules::get() / 2))]
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,
amount: locked_amount,
});
Ok(())
}
#[pallet::call_index(1)]
#[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)?;
if to == from {
ensure!(
T::Currency::free_balance(&from) >= schedule.total_amount().ok_or(ArithmeticError::Overflow)?,
Error::<T>::InsufficientBalanceToLock,
);
}
Self::do_vested_transfer(&from, &to, schedule.clone())?;
Self::deposit_event(Event::VestingScheduleAdded {
from,
to,
vesting_schedule: schedule,
});
Ok(())
}
#[pallet::call_index(2)]
#[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 { who: account });
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::claim(<T as Config>::MaxVestingSchedules::get() / 2))]
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,
amount: 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() {
<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
}
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
})
}
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)?;
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)?;
acc_amount
.checked_add(&amount)
.ok_or_else(|| ArithmeticError::Overflow.into())
})?;
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(())
}
}
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)
}