pallet_moonbeam_orbiters/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
30
31pub mod types;
32pub mod weights;
33
34#[cfg(any(test, feature = "runtime-benchmarks"))]
35mod benchmarks;
36#[cfg(test)]
37mod mock;
38#[cfg(test)]
39mod tests;
40
41pub use pallet::*;
42pub use types::*;
43pub use weights::WeightInfo;
44
45use frame_support::pallet;
46use nimbus_primitives::{AccountLookup, NimbusId};
47
48#[pallet]
49pub mod pallet {
50 use super::*;
51 use frame_support::pallet_prelude::*;
52 use frame_support::traits::{Currency, NamedReservableCurrency};
53 use frame_system::pallet_prelude::*;
54 use sp_runtime::traits::{CheckedSub, One, Saturating, StaticLookup, Zero};
55
56 #[pallet::pallet]
57 #[pallet::without_storage_info]
58 pub struct Pallet<T>(PhantomData<T>);
59
60 pub type BalanceOf<T> =
61 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
62
63 pub type ReserveIdentifierOf<T> = <<T as Config>::Currency as NamedReservableCurrency<
64 <T as frame_system::Config>::AccountId,
65 >>::ReserveIdentifier;
66
67 #[pallet::config]
68 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
69 type AccountLookup: AccountLookup<Self::AccountId>;
72
73 type AddCollatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
75
76 type Currency: NamedReservableCurrency<Self::AccountId>;
78
79 type DelCollatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
81
82 #[pallet::constant]
83 type MaxPoolSize: Get<u32>;
85
86 #[pallet::constant]
87 type MaxRoundArchive: Get<Self::RoundIndex>;
89
90 type OrbiterReserveIdentifier: Get<ReserveIdentifierOf<Self>>;
92
93 #[pallet::constant]
94 type RotatePeriod: Get<Self::RoundIndex>;
98
99 type RoundIndex: Parameter
101 + Member
102 + MaybeSerializeDeserialize
103 + sp_std::fmt::Debug
104 + Default
105 + sp_runtime::traits::MaybeDisplay
106 + sp_runtime::traits::AtLeast32Bit
107 + Copy;
108
109 type WeightInfo: WeightInfo;
111 }
112
113 #[pallet::storage]
114 #[pallet::getter(fn account_lookup_override)]
115 pub type AccountLookupOverride<T: Config> =
117 StorageMap<_, Blake2_128Concat, T::AccountId, Option<T::AccountId>>;
118
119 #[pallet::storage]
120 #[pallet::getter(fn collators_pool)]
121 pub type CollatorsPool<T: Config> =
123 CountedStorageMap<_, Blake2_128Concat, T::AccountId, CollatorPoolInfo<T::AccountId>>;
124
125 #[pallet::storage]
126 pub(crate) type CurrentRound<T: Config> = StorageValue<_, T::RoundIndex, ValueQuery>;
128
129 #[pallet::storage]
130 pub(crate) type ForceRotation<T: Config> = StorageValue<_, bool, ValueQuery>;
134
135 #[pallet::storage]
136 #[pallet::getter(fn min_orbiter_deposit)]
137 pub type MinOrbiterDeposit<T: Config> = StorageValue<_, BalanceOf<T>, OptionQuery>;
139
140 #[pallet::storage]
141 pub(crate) type OrbiterPerRound<T: Config> = StorageDoubleMap<
143 _,
144 Twox64Concat,
145 T::RoundIndex,
146 Blake2_128Concat,
147 T::AccountId,
148 T::AccountId,
149 OptionQuery,
150 >;
151
152 #[pallet::storage]
153 #[pallet::getter(fn orbiter)]
154 pub type RegisteredOrbiter<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, bool>;
156
157 #[pallet::genesis_config]
158 pub struct GenesisConfig<T: Config> {
159 pub min_orbiter_deposit: BalanceOf<T>,
160 }
161
162 impl<T: Config> Default for GenesisConfig<T> {
163 fn default() -> Self {
164 Self {
165 min_orbiter_deposit: One::one(),
166 }
167 }
168 }
169
170 #[pallet::genesis_build]
171 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
172 fn build(&self) {
173 assert!(
174 self.min_orbiter_deposit > Zero::zero(),
175 "Minimal orbiter deposit should be greater than zero"
176 );
177 MinOrbiterDeposit::<T>::put(self.min_orbiter_deposit)
178 }
179 }
180
181 #[pallet::hooks]
182 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
183 fn on_initialize(_: BlockNumberFor<T>) -> Weight {
184 if let Some(round_to_prune) =
186 CurrentRound::<T>::get().checked_sub(&T::MaxRoundArchive::get())
187 {
188 let result = OrbiterPerRound::<T>::clear_prefix(round_to_prune, u32::MAX, None);
194 T::WeightInfo::on_initialize(result.unique)
195 } else {
196 T::DbWeight::get().reads(1)
197 }
198 }
199 }
200
201 #[pallet::error]
203 pub enum Error<T> {
204 CollatorAlreadyAdded,
206 CollatorNotFound,
208 CollatorPoolTooLarge,
210 CollatorsPoolCountTooLow,
212 MinOrbiterDepositNotSet,
215 OrbiterAlreadyInPool,
217 OrbiterDepositNotFound,
219 OrbiterNotFound,
221 OrbiterStillInAPool,
223 }
224
225 #[pallet::event]
226 #[pallet::generate_deposit(pub(crate) fn deposit_event)]
227 pub enum Event<T: Config> {
228 OrbiterJoinCollatorPool {
230 collator: T::AccountId,
231 orbiter: T::AccountId,
232 },
233 OrbiterLeaveCollatorPool {
235 collator: T::AccountId,
236 orbiter: T::AccountId,
237 },
238 OrbiterRewarded {
240 account: T::AccountId,
241 rewards: BalanceOf<T>,
242 },
243 OrbiterRotation {
244 collator: T::AccountId,
245 old_orbiter: Option<T::AccountId>,
246 new_orbiter: Option<T::AccountId>,
247 },
248 OrbiterRegistered {
250 account: T::AccountId,
251 deposit: BalanceOf<T>,
252 },
253 OrbiterUnregistered { account: T::AccountId },
255 }
256
257 #[pallet::call]
258 impl<T: Config> Pallet<T> {
259 #[pallet::call_index(0)]
261 #[pallet::weight(T::WeightInfo::collator_add_orbiter())]
262 pub fn collator_add_orbiter(
263 origin: OriginFor<T>,
264 orbiter: <T::Lookup as StaticLookup>::Source,
265 ) -> DispatchResult {
266 let collator = ensure_signed(origin)?;
267 let orbiter = T::Lookup::lookup(orbiter)?;
268
269 let mut collator_pool =
270 CollatorsPool::<T>::get(&collator).ok_or(Error::<T>::CollatorNotFound)?;
271 let orbiters = collator_pool.get_orbiters();
272 ensure!(
273 (orbiters.len() as u32) < T::MaxPoolSize::get(),
274 Error::<T>::CollatorPoolTooLarge
275 );
276 if orbiters.iter().any(|orbiter_| orbiter_ == &orbiter) {
277 return Err(Error::<T>::OrbiterAlreadyInPool.into());
278 }
279
280 let orbiter_deposit =
285 T::Currency::reserved_balance_named(&T::OrbiterReserveIdentifier::get(), &orbiter);
286 ensure!(
287 orbiter_deposit > BalanceOf::<T>::zero(),
288 Error::<T>::OrbiterDepositNotFound
289 );
290
291 collator_pool.add_orbiter(orbiter.clone());
292 CollatorsPool::<T>::insert(&collator, collator_pool);
293
294 Self::deposit_event(Event::OrbiterJoinCollatorPool { collator, orbiter });
295
296 Ok(())
297 }
298
299 #[pallet::call_index(1)]
301 #[pallet::weight(T::WeightInfo::collator_remove_orbiter())]
302 pub fn collator_remove_orbiter(
303 origin: OriginFor<T>,
304 orbiter: <T::Lookup as StaticLookup>::Source,
305 ) -> DispatchResult {
306 let collator = ensure_signed(origin)?;
307 let orbiter = T::Lookup::lookup(orbiter)?;
308
309 Self::do_remove_orbiter_from_pool(collator, orbiter)
310 }
311
312 #[pallet::call_index(2)]
314 #[pallet::weight(T::WeightInfo::orbiter_leave_collator_pool())]
315 pub fn orbiter_leave_collator_pool(
316 origin: OriginFor<T>,
317 collator: <T::Lookup as StaticLookup>::Source,
318 ) -> DispatchResult {
319 let orbiter = ensure_signed(origin)?;
320 let collator = T::Lookup::lookup(collator)?;
321
322 Self::do_remove_orbiter_from_pool(collator, orbiter)
323 }
324
325 #[pallet::call_index(3)]
327 #[pallet::weight(T::WeightInfo::orbiter_register())]
328 pub fn orbiter_register(origin: OriginFor<T>) -> DispatchResult {
329 let orbiter = ensure_signed(origin)?;
330
331 if let Some(min_orbiter_deposit) = MinOrbiterDeposit::<T>::get() {
332 T::Currency::ensure_reserved_named(
335 &T::OrbiterReserveIdentifier::get(),
336 &orbiter,
337 min_orbiter_deposit,
338 )?;
339 RegisteredOrbiter::<T>::insert(&orbiter, true);
340 Self::deposit_event(Event::OrbiterRegistered {
341 account: orbiter,
342 deposit: min_orbiter_deposit,
343 });
344 Ok(())
345 } else {
346 Err(Error::<T>::MinOrbiterDepositNotSet.into())
347 }
348 }
349
350 #[pallet::call_index(4)]
352 #[pallet::weight(T::WeightInfo::orbiter_unregister(*collators_pool_count))]
353 pub fn orbiter_unregister(
354 origin: OriginFor<T>,
355 collators_pool_count: u32,
356 ) -> DispatchResult {
357 let orbiter = ensure_signed(origin)?;
358
359 ensure!(
362 collators_pool_count >= CollatorsPool::<T>::count(),
363 Error::<T>::CollatorsPoolCountTooLow
364 );
365
366 ensure!(
368 !CollatorsPool::<T>::iter_values()
369 .any(|collator_pool| collator_pool.contains_orbiter(&orbiter)),
370 Error::<T>::OrbiterStillInAPool,
371 );
372
373 T::Currency::unreserve_all_named(&T::OrbiterReserveIdentifier::get(), &orbiter);
374 RegisteredOrbiter::<T>::remove(&orbiter);
375 Self::deposit_event(Event::OrbiterUnregistered { account: orbiter });
376
377 Ok(())
378 }
379
380 #[pallet::call_index(5)]
382 #[pallet::weight(T::WeightInfo::add_collator())]
383 pub fn add_collator(
384 origin: OriginFor<T>,
385 collator: <T::Lookup as StaticLookup>::Source,
386 ) -> DispatchResult {
387 T::AddCollatorOrigin::ensure_origin(origin)?;
388 let collator = T::Lookup::lookup(collator)?;
389
390 ensure!(
391 CollatorsPool::<T>::get(&collator).is_none(),
392 Error::<T>::CollatorAlreadyAdded
393 );
394
395 CollatorsPool::<T>::insert(collator, CollatorPoolInfo::default());
396
397 Ok(())
398 }
399
400 #[pallet::call_index(6)]
402 #[pallet::weight(T::WeightInfo::remove_collator())]
403 pub fn remove_collator(
404 origin: OriginFor<T>,
405 collator: <T::Lookup as StaticLookup>::Source,
406 ) -> DispatchResult {
407 T::DelCollatorOrigin::ensure_origin(origin)?;
408 let collator = T::Lookup::lookup(collator)?;
409
410 let collator_pool =
412 CollatorsPool::<T>::take(&collator).ok_or(Error::<T>::CollatorNotFound)?;
413
414 for orbiter in collator_pool.get_orbiters() {
416 AccountLookupOverride::<T>::remove(&orbiter);
417 }
418 AccountLookupOverride::<T>::remove(&collator);
419
420 Ok(())
421 }
422 }
423
424 impl<T: Config> Pallet<T> {
425 fn do_remove_orbiter_from_pool(
426 collator: T::AccountId,
427 orbiter: T::AccountId,
428 ) -> DispatchResult {
429 let mut collator_pool =
430 CollatorsPool::<T>::get(&collator).ok_or(Error::<T>::CollatorNotFound)?;
431
432 match collator_pool.remove_orbiter(&orbiter) {
433 RemoveOrbiterResult::OrbiterNotFound => {
434 return Err(Error::<T>::OrbiterNotFound.into())
435 }
436 RemoveOrbiterResult::OrbiterRemoved => {
437 Self::deposit_event(Event::OrbiterLeaveCollatorPool {
438 collator: collator.clone(),
439 orbiter,
440 });
441 }
442 RemoveOrbiterResult::OrbiterRemoveScheduled => (),
443 }
444
445 CollatorsPool::<T>::insert(collator, collator_pool);
446 Ok(())
447 }
448 fn on_rotate(round_index: T::RoundIndex) -> Weight {
449 let mut writes = 1;
450 CollatorsPool::<T>::translate::<CollatorPoolInfo<T::AccountId>, _>(
452 |collator, mut pool| {
453 let RotateOrbiterResult {
454 maybe_old_orbiter,
455 maybe_next_orbiter,
456 } = pool.rotate_orbiter();
457
458 if let Some(CurrentOrbiter {
460 account_id: ref current_orbiter,
461 removed,
462 }) = maybe_old_orbiter
463 {
464 if removed {
465 Self::deposit_event(Event::OrbiterLeaveCollatorPool {
466 collator: collator.clone(),
467 orbiter: current_orbiter.clone(),
468 });
469 }
470 AccountLookupOverride::<T>::remove(current_orbiter.clone());
471 writes += 1;
472 }
473 if let Some(next_orbiter) = maybe_next_orbiter {
474 AccountLookupOverride::<T>::insert(
476 collator.clone(),
477 Option::<T::AccountId>::None,
478 );
479 writes += 1;
480
481 AccountLookupOverride::<T>::insert(
483 next_orbiter.clone(),
484 Some(collator.clone()),
485 );
486 writes += 1;
487
488 let mut i = Zero::zero();
489 while i < T::RotatePeriod::get() {
490 OrbiterPerRound::<T>::insert(
491 round_index.saturating_add(i),
492 collator.clone(),
493 next_orbiter.clone(),
494 );
495 i += One::one();
496 writes += 1;
497 }
498
499 Self::deposit_event(Event::OrbiterRotation {
500 collator,
501 old_orbiter: maybe_old_orbiter.map(|orbiter| orbiter.account_id),
502 new_orbiter: Some(next_orbiter),
503 });
504 } else {
505 AccountLookupOverride::<T>::remove(collator.clone());
507 writes += 1;
508 Self::deposit_event(Event::OrbiterRotation {
509 collator,
510 old_orbiter: maybe_old_orbiter.map(|orbiter| orbiter.account_id),
511 new_orbiter: None,
512 });
513 }
514 writes += 1;
515 Some(pool)
516 },
517 );
518 T::DbWeight::get().reads_writes(1, writes)
519 }
520 pub fn on_new_round(round_index: T::RoundIndex) -> Weight {
522 CurrentRound::<T>::put(round_index);
523
524 if ForceRotation::<T>::get() {
525 ForceRotation::<T>::put(false);
526 let _ = Self::on_rotate(round_index);
527 T::WeightInfo::on_new_round()
528 } else if round_index % T::RotatePeriod::get() == Zero::zero() {
529 let _ = Self::on_rotate(round_index);
530 T::WeightInfo::on_new_round()
531 } else {
532 T::DbWeight::get().writes(1)
533 }
534 }
535 pub fn distribute_rewards(
537 pay_for_round: T::RoundIndex,
538 collator: T::AccountId,
539 amount: BalanceOf<T>,
540 ) -> Weight {
541 if let Some(orbiter) = OrbiterPerRound::<T>::take(pay_for_round, &collator) {
542 if T::Currency::deposit_into_existing(&orbiter, amount).is_ok() {
543 Self::deposit_event(Event::OrbiterRewarded {
544 account: orbiter,
545 rewards: amount,
546 });
547 }
548 T::WeightInfo::distribute_rewards()
549 } else {
550 T::DbWeight::get().writes(1)
552 }
553 }
554
555 pub fn is_collator_pool_with_active_orbiter(
558 for_round: T::RoundIndex,
559 collator: T::AccountId,
560 ) -> bool {
561 OrbiterPerRound::<T>::contains_key(for_round, &collator)
562 }
563 }
564}
565
566impl<T: Config> AccountLookup<T::AccountId> for Pallet<T> {
567 fn lookup_account(nimbus_id: &NimbusId) -> Option<T::AccountId> {
568 let account_id = T::AccountLookup::lookup_account(nimbus_id)?;
569 match AccountLookupOverride::<T>::get(&account_id) {
570 Some(override_) => override_,
571 None => Some(account_id),
572 }
573 }
574}