1#![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#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo, DecodeWithMemTracking)]
62pub struct VestingSchedule<BlockNumber, Balance>
63where
64 Balance: MaxEncodedLen + HasCompact,
65{
66 pub start: BlockNumber,
68 pub period: BlockNumber,
70 pub period_count: u32,
72 #[codec(compact)]
74 pub per_period: Balance,
75}
76
77impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + MaxEncodedLen + Copy>
78 VestingSchedule<BlockNumber, Balance>
79{
80 pub fn end(&self) -> Option<BlockNumber> {
82 self.period
84 .checked_mul(&self.period_count.into())?
85 .checked_add(&self.start)
86 }
87
88 pub fn total_amount(&self) -> Option<Balance> {
90 self.per_period.checked_mul(&self.period_count.into())
91 }
92
93 pub fn locked_amount(&self, time: BlockNumber) -> Balance {
98 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 type MinVestedTransfer: Get<BalanceOf<Self>>;
134
135 type VestedTransferOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
137
138 type WeightInfo: WeightInfo;
140
141 type MaxVestingSchedules: Get<u32>;
143
144 type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
146 }
147
148 #[pallet::error]
149 pub enum Error<T> {
150 ZeroVestingPeriod,
152 ZeroVestingPeriodCount,
154 InsufficientBalanceToLock,
156 TooManyVestingSchedules,
158 AmountLow,
160 MaxVestingSchedulesExceeded,
162 }
163
164 #[pallet::event]
165 #[pallet::generate_deposit(fn deposit_event)]
166 pub enum Event<T: Config> {
167 VestingScheduleAdded {
169 from: T::AccountId,
170 to: T::AccountId,
171 vesting_schedule: VestingScheduleOf<T>,
172 },
173 Claimed { who: T::AccountId, amount: BalanceOf<T> },
175 VestingSchedulesUpdated { who: T::AccountId },
177 }
178
179 #[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 <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 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 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
401fn 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}