1#![cfg_attr(not(feature = "std"), no_std)]
21
22use core::marker::PhantomData;
23use frame::{
24 prelude::*,
25 traits::tokens::{GetSalary, Pay, PaymentStatus},
26};
27
28#[cfg(test)]
29mod tests;
30
31#[cfg(feature = "runtime-benchmarks")]
32mod benchmarking;
33pub mod weights;
34
35pub use pallet::*;
36pub use weights::WeightInfo;
37
38pub type Cycle = u32;
40
41#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
43pub struct StatusType<CycleIndex, BlockNumber, Balance> {
44 cycle_index: CycleIndex,
46 cycle_start: BlockNumber,
48 budget: Balance,
50 total_registrations: Balance,
52 total_unregistered_paid: Balance,
54}
55
56#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
58pub enum ClaimState<Balance, Id> {
59 Nothing,
61 Registered(Balance),
63 Attempted { registered: Option<Balance>, id: Id, amount: Balance },
65}
66
67use ClaimState::*;
68
69#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
71pub struct ClaimantStatus<CycleIndex, Balance, Id> {
72 last_active: CycleIndex,
74 status: ClaimState<Balance, Id>,
76}
77
78#[frame::pallet]
79pub mod pallet {
80 use super::*;
81 #[pallet::pallet]
82 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
83
84 #[pallet::config]
85 pub trait Config<I: 'static = ()>: frame_system::Config {
86 type WeightInfo: WeightInfo;
88
89 #[allow(deprecated)]
91 type RuntimeEvent: From<Event<Self, I>>
92 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
93
94 type Paymaster: Pay<Beneficiary = <Self as frame_system::Config>::AccountId, AssetKind = ()>;
97
98 type Members: RankedMembers<AccountId = <Self as frame_system::Config>::AccountId>;
100
101 type Salary: GetSalary<
105 <Self::Members as RankedMembers>::Rank,
106 Self::AccountId,
107 <Self::Paymaster as Pay>::Balance,
108 >;
109
110 #[pallet::constant]
116 type RegistrationPeriod: Get<BlockNumberFor<Self>>;
117
118 #[pallet::constant]
123 type PayoutPeriod: Get<BlockNumberFor<Self>>;
124
125 #[pallet::constant]
129 type Budget: Get<BalanceOf<Self, I>>;
130 }
131
132 pub type CycleIndexOf<T> = BlockNumberFor<T>;
133 pub type BalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
134 pub type IdOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Id;
135 pub type StatusOf<T, I> = StatusType<CycleIndexOf<T>, BlockNumberFor<T>, BalanceOf<T, I>>;
136 pub type ClaimantStatusOf<T, I> = ClaimantStatus<CycleIndexOf<T>, BalanceOf<T, I>, IdOf<T, I>>;
137
138 #[pallet::storage]
140 pub type Status<T: Config<I>, I: 'static = ()> = StorageValue<_, StatusOf<T, I>, OptionQuery>;
141
142 #[pallet::storage]
144 pub type Claimant<T: Config<I>, I: 'static = ()> =
145 StorageMap<_, Twox64Concat, T::AccountId, ClaimantStatusOf<T, I>, OptionQuery>;
146
147 #[pallet::event]
148 #[pallet::generate_deposit(pub(super) fn deposit_event)]
149 pub enum Event<T: Config<I>, I: 'static = ()> {
150 Inducted { who: T::AccountId },
152 Registered { who: T::AccountId, amount: BalanceOf<T, I> },
154 Paid {
156 who: T::AccountId,
157 beneficiary: T::AccountId,
158 amount: BalanceOf<T, I>,
159 id: <T::Paymaster as Pay>::Id,
160 },
161 CycleStarted { index: CycleIndexOf<T> },
163 Swapped { who: T::AccountId, new_who: T::AccountId },
165 }
166
167 #[pallet::error]
168 pub enum Error<T, I = ()> {
169 AlreadyStarted,
171 NotMember,
173 AlreadyInducted,
175 NotInducted,
177 NoClaim,
179 ClaimZero,
181 TooLate,
183 TooEarly,
185 NotYet,
187 NotStarted,
189 Bankrupt,
191 PayError,
193 Inconclusive,
195 NotCurrent,
197 }
198
199 #[pallet::call]
200 impl<T: Config<I>, I: 'static> Pallet<T, I> {
201 #[pallet::weight(T::WeightInfo::init())]
205 #[pallet::call_index(0)]
206 pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
207 ensure_signed(origin)?;
208 let now = frame_system::Pallet::<T>::block_number();
209 ensure!(!Status::<T, I>::exists(), Error::<T, I>::AlreadyStarted);
210 let status = StatusType {
211 cycle_index: Zero::zero(),
212 cycle_start: now,
213 budget: T::Budget::get(),
214 total_registrations: Zero::zero(),
215 total_unregistered_paid: Zero::zero(),
216 };
217 Status::<T, I>::put(&status);
218
219 Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
220 Ok(Pays::No.into())
221 }
222
223 #[pallet::weight(T::WeightInfo::bump())]
227 #[pallet::call_index(1)]
228 pub fn bump(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
229 ensure_signed(origin)?;
230 let now = frame_system::Pallet::<T>::block_number();
231 let cycle_period = Self::cycle_period();
232 let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
233 status.cycle_start.saturating_accrue(cycle_period);
234 ensure!(now >= status.cycle_start, Error::<T, I>::NotYet);
235 status.cycle_index.saturating_inc();
236 status.budget = T::Budget::get();
237 status.total_registrations = Zero::zero();
238 status.total_unregistered_paid = Zero::zero();
239 Status::<T, I>::put(&status);
240
241 Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
242 Ok(Pays::No.into())
243 }
244
245 #[pallet::weight(T::WeightInfo::induct())]
247 #[pallet::call_index(2)]
248 pub fn induct(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
249 let who = ensure_signed(origin)?;
250 let cycle_index = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?.cycle_index;
251 T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
252 ensure!(!Claimant::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
253
254 Claimant::<T, I>::insert(
255 &who,
256 ClaimantStatus { last_active: cycle_index, status: Nothing },
257 );
258
259 Self::deposit_event(Event::<T, I>::Inducted { who });
260 Ok(Pays::No.into())
261 }
262
263 #[pallet::weight(T::WeightInfo::register())]
270 #[pallet::call_index(3)]
271 pub fn register(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
272 let who = ensure_signed(origin)?;
273 let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
274 let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
275 let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
276 let now = frame_system::Pallet::<T>::block_number();
277 ensure!(
278 now < status.cycle_start + T::RegistrationPeriod::get(),
279 Error::<T, I>::TooLate
280 );
281 ensure!(claimant.last_active < status.cycle_index, Error::<T, I>::NoClaim);
282 let payout = T::Salary::get_salary(rank, &who);
283 ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
284 claimant.last_active = status.cycle_index;
285 claimant.status = Registered(payout);
286 status.total_registrations.saturating_accrue(payout);
287
288 Claimant::<T, I>::insert(&who, &claimant);
289 Status::<T, I>::put(&status);
290
291 Self::deposit_event(Event::<T, I>::Registered { who, amount: payout });
292 Ok(Pays::No.into())
293 }
294
295 #[pallet::weight(T::WeightInfo::payout())]
302 #[pallet::call_index(4)]
303 pub fn payout(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
304 let who = ensure_signed(origin)?;
305 Self::do_payout(who.clone(), who)?;
306 Ok(Pays::No.into())
307 }
308
309 #[pallet::weight(T::WeightInfo::payout_other())]
317 #[pallet::call_index(5)]
318 pub fn payout_other(
319 origin: OriginFor<T>,
320 beneficiary: T::AccountId,
321 ) -> DispatchResultWithPostInfo {
322 let who = ensure_signed(origin)?;
323 Self::do_payout(who, beneficiary)?;
324 Ok(Pays::No.into())
325 }
326
327 #[pallet::weight(T::WeightInfo::check_payment())]
335 #[pallet::call_index(6)]
336 pub fn check_payment(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
337 let who = ensure_signed(origin)?;
338
339 let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
340 let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
341 ensure!(claimant.last_active == status.cycle_index, Error::<T, I>::NotCurrent);
342 let (id, registered, amount) = match claimant.status {
343 Attempted { id, registered, amount } => (id, registered, amount),
344 _ => return Err(Error::<T, I>::NoClaim.into()),
345 };
346 match T::Paymaster::check_payment(id) {
347 PaymentStatus::Failure => {
348 if let Some(amount) = registered {
350 claimant.status = ClaimState::Registered(amount);
352 } else {
353 claimant.last_active.saturating_reduce(1u32.into());
356 claimant.status = ClaimState::Nothing;
357 status.total_unregistered_paid.saturating_reduce(amount);
360 }
361 },
362 PaymentStatus::Success => claimant.status = ClaimState::Nothing,
363 _ => return Err(Error::<T, I>::Inconclusive.into()),
364 }
365 Claimant::<T, I>::insert(&who, &claimant);
366 Status::<T, I>::put(&status);
367
368 Ok(Pays::No.into())
369 }
370 }
371
372 impl<T: Config<I>, I: 'static> Pallet<T, I> {
373 pub fn status() -> Option<StatusOf<T, I>> {
374 Status::<T, I>::get()
375 }
376 pub fn last_active(who: &T::AccountId) -> Result<CycleIndexOf<T>, DispatchError> {
377 Ok(Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?.last_active)
378 }
379 pub fn cycle_period() -> BlockNumberFor<T> {
380 T::RegistrationPeriod::get() + T::PayoutPeriod::get()
381 }
382 fn do_payout(who: T::AccountId, beneficiary: T::AccountId) -> DispatchResult {
383 let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
384 let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
385
386 let now = frame_system::Pallet::<T>::block_number();
387 ensure!(
388 now >= status.cycle_start + T::RegistrationPeriod::get(),
389 Error::<T, I>::TooEarly,
390 );
391
392 let (payout, registered) = match claimant.status {
393 Registered(unpaid) if claimant.last_active == status.cycle_index => {
394 let payout = if status.total_registrations <= status.budget {
396 unpaid
398 } else {
399 Perbill::from_rational(status.budget, status.total_registrations)
401 .mul_floor(unpaid)
402 };
403 (payout, Some(unpaid))
404 },
405 Nothing | Attempted { .. } if claimant.last_active < status.cycle_index => {
406 let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
408 let ideal_payout = T::Salary::get_salary(rank, &who);
409
410 let pot = status
411 .budget
412 .saturating_sub(status.total_registrations)
413 .saturating_sub(status.total_unregistered_paid);
414
415 let payout = ideal_payout.min(pot);
416 ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
417
418 status.total_unregistered_paid.saturating_accrue(payout);
419 (payout, None)
420 },
421 _ => return Err(Error::<T, I>::NoClaim.into()),
422 };
423
424 claimant.last_active = status.cycle_index;
425
426 let id =
427 T::Paymaster::pay(&beneficiary, (), payout).map_err(|_| Error::<T, I>::PayError)?;
428
429 claimant.status = Attempted { registered, id, amount: payout };
430
431 Claimant::<T, I>::insert(&who, &claimant);
432 Status::<T, I>::put(&status);
433
434 Self::deposit_event(Event::<T, I>::Paid { who, beneficiary, amount: payout, id });
435 Ok(())
436 }
437 }
438}
439
440impl<T: Config<I>, I: 'static>
441 RankedMembersSwapHandler<T::AccountId, <T::Members as RankedMembers>::Rank> for Pallet<T, I>
442{
443 fn swapped(
444 who: &T::AccountId,
445 new_who: &T::AccountId,
446 _rank: <T::Members as RankedMembers>::Rank,
447 ) {
448 if who == new_who {
449 defensive!("Should not try to swap with self");
450 return;
451 }
452 if Claimant::<T, I>::contains_key(new_who) {
453 defensive!("Should not try to overwrite existing claimant");
454 return;
455 }
456
457 let Some(claimant) = Claimant::<T, I>::take(who) else {
458 defensive!("Claimant should exist when swapping");
459 return;
460 };
461
462 Claimant::<T, I>::insert(new_who, claimant);
463 Self::deposit_event(Event::<T, I>::Swapped { who: who.clone(), new_who: new_who.clone() });
464 }
465}
466
467#[cfg(feature = "runtime-benchmarks")]
468impl<T: Config<I>, I: 'static>
469 pallet_ranked_collective::BenchmarkSetup<<T as frame_system::Config>::AccountId> for Pallet<T, I>
470{
471 fn ensure_member(who: &<T as frame_system::Config>::AccountId) {
472 Self::init(frame_system::RawOrigin::Signed(who.clone()).into()).unwrap();
473 Self::induct(frame_system::RawOrigin::Signed(who.clone()).into()).unwrap();
474 }
475}