123456789101112131415161718192021222324252627#![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";575859606162#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]63pub struct VestingSchedule<BlockNumber, Balance: HasCompact> {64 65 pub start: BlockNumber,66 67 pub period: BlockNumber,68 69 pub period_count: u32,70 71 #[codec(compact)]72 pub per_period: Balance,73}7475impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + Copy> VestingSchedule<BlockNumber, Balance> {76 77 pub fn end(&self) -> Option<BlockNumber> {78 79 self.period80 .checked_mul(&self.period_count.into())?81 .checked_add(&self.start)82 }8384 85 pub fn total_amount(&self) -> Option<Balance> {86 self.per_period.checked_mul(&self.period_count.into())87 }8889 90 91 92 93 pub fn locked_amount(&self, time: BlockNumber) -> Balance {94 95 96 97 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 131 type MinVestedTransfer: Get<BalanceOf<Self>>;132133 134 type VestedTransferOrigin: EnsureOrigin<Self::Origin, Success = Self::AccountId>;135136 137 type WeightInfo: WeightInfo;138139 140 type MaxVestingSchedules: Get<u32>;141142 143 type BlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;144 }145146 #[pallet::error]147 pub enum Error<T> {148 149 ZeroVestingPeriod,150 151 ZeroVestingPeriodCount,152 153 InsufficientBalanceToLock,154 155 TooManyVestingSchedules,156 157 AmountLow,158 159 MaxVestingSchedulesExceeded,160 }161162 #[pallet::event]163 #[pallet::generate_deposit(fn deposit_event)]164 pub enum Event<T: Config> {165 166 VestingScheduleAdded(T::AccountId, T::AccountId, VestingScheduleOf<T>),167 168 Claimed(T::AccountId, BalanceOf<T>),169 170 VestingSchedulesUpdated(T::AccountId),171 }172173 174 175 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 295 <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 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 345 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}368369370fn 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}