1#![cfg_attr(not(feature = "std"), no_std)]
29
30pub use crate::weights::WeightInfo;
31use frame_support::pallet;
32pub use pallet::*;
33
34#[cfg(any(test, feature = "runtime-benchmarks"))]
35mod benchmarks;
36#[cfg(test)]
37mod mock;
38#[cfg(test)]
39mod tests;
40pub mod weights;
41
42#[pallet]
43pub mod pallet {
44 use super::*;
45 use frame_support::{
46 pallet_prelude::*,
47 traits::{Currency, ExistenceRequirement::AllowDeath, WithdrawReasons},
48 PalletId,
49 };
50 use frame_system::pallet_prelude::*;
51 use parity_scale_codec::{Decode, Encode};
52 use sp_core::crypto::AccountId32;
53 use sp_runtime::traits::{
54 AccountIdConversion, AtLeast32BitUnsigned, BlockNumberProvider, Saturating, Verify,
55 };
56 use sp_runtime::{MultiSignature, Perbill};
57 use sp_std::collections::btree_map::BTreeMap;
58 use sp_std::vec;
59 use sp_std::vec::Vec;
60 #[pallet::pallet]
61 #[pallet::without_storage_info]
62 pub struct Pallet<T>(PhantomData<T>);
64
65 pub const PALLET_ID: PalletId = PalletId(*b"Crowdloa");
66
67 pub const WRAPPED_BYTES_PREFIX: &[u8] = b"<Bytes>";
69 pub const WRAPPED_BYTES_POSTFIX: &[u8] = b"</Bytes>";
70
71 #[pallet::config]
73 pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
74 type Initialized: Get<bool>;
76 #[pallet::constant]
78 type InitializationPayment: Get<Perbill>;
79 #[pallet::constant]
81 type MaxInitContributors: Get<u32>;
82 type MinimumReward: Get<BalanceOf<Self>>;
84 #[pallet::constant]
87 type RewardAddressRelayVoteThreshold: Get<Perbill>;
88 type RewardCurrency: Currency<Self::AccountId>;
90 type RelayChainAccountId: Parameter
92 + Into<AccountId32>
94 + From<AccountId32>
95 + Ord
96 + sp_runtime::serde::Serialize
97 + for<'a> sp_runtime::serde::Deserialize<'a>;
98
99 type RewardAddressChangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
101
102 #[pallet::constant]
105 type SignatureNetworkIdentifier: Get<&'static [u8]>;
106
107 type RewardAddressAssociateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
109
110 type VestingBlockNumber: AtLeast32BitUnsigned
112 + Parameter
113 + Default
114 + Into<BalanceOf<Self>>
115 + sp_runtime::serde::Serialize
116 + for<'a> sp_runtime::serde::Deserialize<'a>;
117
118 type VestingBlockProvider: BlockNumberProvider<BlockNumber = Self::VestingBlockNumber>;
121
122 type WeightInfo: WeightInfo;
123 }
124
125 pub type BalanceOf<T> = <<T as Config>::RewardCurrency as Currency<
126 <T as frame_system::Config>::AccountId,
127 >>::Balance;
128
129 pub type ContributorData<T> = (
131 <T as Config>::RelayChainAccountId,
132 Option<<T as frame_system::Config>::AccountId>,
133 BalanceOf<T>,
134 );
135
136 #[derive(Default, Clone, Encode, Decode, RuntimeDebug, PartialEq, scale_info::TypeInfo)]
140 #[scale_info(skip_type_params(T))]
141 pub struct RewardInfo<T: Config> {
142 pub total_reward: BalanceOf<T>,
143 pub claimed_reward: BalanceOf<T>,
144 pub contributed_relay_addresses: Vec<T::RelayChainAccountId>,
145 }
146
147 #[pallet::call]
148 impl<T: Config> Pallet<T> {
149 #[pallet::call_index(0)]
155 #[pallet::weight(T::WeightInfo::associate_native_identity())]
156 pub fn associate_native_identity(
157 origin: OriginFor<T>,
158 reward_account: T::AccountId,
159 relay_account: T::RelayChainAccountId,
160 proof: MultiSignature,
161 ) -> DispatchResultWithPostInfo {
162 T::RewardAddressChangeOrigin::ensure_origin(origin)?;
164
165 let mut reward_info = UnassociatedContributions::<T>::get(&relay_account)
175 .ok_or(Error::<T>::NoAssociatedClaim)?;
176
177 ensure!(
180 ClaimedRelayChainIds::<T>::get(&relay_account).is_none(),
181 Error::<T>::AlreadyAssociated
182 );
183
184 ensure!(
186 AccountsPayable::<T>::get(&reward_account).is_none(),
187 Error::<T>::AlreadyAssociated
188 );
189
190 let mut payload = WRAPPED_BYTES_PREFIX.to_vec();
192 payload.append(&mut T::SignatureNetworkIdentifier::get().to_vec());
193 payload.append(&mut reward_account.encode());
194 payload.append(&mut WRAPPED_BYTES_POSTFIX.to_vec());
195
196 Self::verify_signatures(
198 vec![(relay_account.clone(), proof)],
199 reward_info.clone(),
200 payload,
201 )?;
202
203 let first_payment = T::InitializationPayment::get() * reward_info.total_reward;
205
206 T::RewardCurrency::transfer(
207 &PALLET_ID.into_account_truncating(),
208 &reward_account,
209 first_payment,
210 AllowDeath,
211 )?;
212
213 Self::deposit_event(Event::InitialPaymentMade(
214 reward_account.clone(),
215 first_payment,
216 ));
217
218 reward_info.claimed_reward = first_payment;
219
220 AccountsPayable::<T>::insert(&reward_account, &reward_info);
222
223 <UnassociatedContributions<T>>::remove(&relay_account);
225
226 ClaimedRelayChainIds::<T>::insert(&relay_account, ());
228
229 Self::deposit_event(Event::NativeIdentityAssociated(
231 relay_account,
232 reward_account,
233 reward_info.total_reward,
234 ));
235
236 Ok(Default::default())
237 }
238
239 #[pallet::call_index(1)]
245 #[pallet::weight(T::WeightInfo::change_association_with_relay_keys(proofs.len() as u32))]
246 pub fn change_association_with_relay_keys(
247 origin: OriginFor<T>,
248 reward_account: T::AccountId,
249 previous_account: T::AccountId,
250 proofs: Vec<(T::RelayChainAccountId, MultiSignature)>,
251 ) -> DispatchResultWithPostInfo {
252 T::RewardAddressChangeOrigin::ensure_origin(origin)?;
254
255 ensure!(
257 AccountsPayable::<T>::get(&reward_account).is_none(),
258 Error::<T>::AlreadyAssociated
259 );
260
261 let mut payload = WRAPPED_BYTES_PREFIX.to_vec();
265 payload.append(&mut T::SignatureNetworkIdentifier::get().to_vec());
266 payload.append(&mut reward_account.encode());
267 payload.append(&mut previous_account.encode());
268 payload.append(&mut WRAPPED_BYTES_POSTFIX.to_vec());
269
270 let reward_info = AccountsPayable::<T>::get(&previous_account)
272 .ok_or(Error::<T>::NoAssociatedClaim)?;
273
274 Self::verify_signatures(proofs, reward_info.clone(), payload)?;
275
276 AccountsPayable::<T>::remove(&previous_account);
278
279 AccountsPayable::<T>::insert(&reward_account, &reward_info);
281
282 Self::deposit_event(Event::RewardAddressUpdated(
284 previous_account,
285 reward_account,
286 ));
287
288 Ok(Default::default())
289 }
290
291 #[pallet::call_index(2)]
293 #[pallet::weight(T::WeightInfo::claim())]
294 pub fn claim(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
295 let payee = ensure_signed(origin)?;
296 let initialized = <Initialized<T>>::get();
297 ensure!(initialized, Error::<T>::RewardVecNotFullyInitializedYet);
298 let mut info =
300 AccountsPayable::<T>::get(&payee).ok_or(Error::<T>::NoAssociatedClaim)?;
301 ensure!(
302 info.claimed_reward < info.total_reward,
303 Error::<T>::RewardsAlreadyClaimed
304 );
305
306 let now = T::VestingBlockProvider::current_block_number();
308
309 let first_paid = T::InitializationPayment::get() * info.total_reward;
311
312 let payable_period = now.saturating_sub(<InitVestingBlock<T>>::get());
314
315 let period = EndVestingBlock::<T>::get() - InitVestingBlock::<T>::get();
318 let should_have_claimed = if period == 0u32.into() {
319 info.total_reward - first_paid
321 } else {
322 (info.total_reward - first_paid).saturating_mul(payable_period.into())
323 / period.into()
324 };
325
326 let payable_amount = if should_have_claimed >= (info.total_reward - first_paid) {
328 info.total_reward.saturating_sub(info.claimed_reward)
329 } else {
330 should_have_claimed + first_paid - info.claimed_reward
331 };
332
333 info.claimed_reward = info.claimed_reward.saturating_add(payable_amount);
334 AccountsPayable::<T>::insert(&payee, &info);
335
336 T::RewardCurrency::transfer(
339 &PALLET_ID.into_account_truncating(),
340 &payee,
341 payable_amount,
342 AllowDeath,
343 )?;
344 Self::deposit_event(Event::RewardsPaid(payee, payable_amount));
346 Ok(Default::default())
347 }
348
349 #[pallet::call_index(3)]
351 #[pallet::weight(T::WeightInfo::update_reward_address())]
352 pub fn update_reward_address(
353 origin: OriginFor<T>,
354 new_reward_account: T::AccountId,
355 ) -> DispatchResultWithPostInfo {
356 let signer = ensure_signed(origin)?;
357
358 let info = AccountsPayable::<T>::get(&signer).ok_or(Error::<T>::NoAssociatedClaim)?;
360
361 ensure!(
363 AccountsPayable::<T>::get(&new_reward_account).is_none(),
364 Error::<T>::AlreadyAssociated
365 );
366
367 AccountsPayable::<T>::remove(&signer);
369
370 AccountsPayable::<T>::insert(&new_reward_account, &info);
372
373 Self::deposit_event(Event::RewardAddressUpdated(signer, new_reward_account));
375
376 Ok(Default::default())
377 }
378 }
379
380 impl<T: Config> Pallet<T> {
381 pub fn account_id() -> T::AccountId {
383 PALLET_ID.into_account_truncating()
384 }
385 pub fn pot() -> BalanceOf<T> {
387 T::RewardCurrency::free_balance(&Self::account_id())
388 }
389 #[allow(clippy::map_entry)] fn verify_signatures(
396 proofs: Vec<(T::RelayChainAccountId, MultiSignature)>,
397 reward_info: RewardInfo<T>,
398 payload: Vec<u8>,
399 ) -> DispatchResult {
400 let mut voted: BTreeMap<T::RelayChainAccountId, ()> = BTreeMap::new();
407 for (relay_account, signature) in proofs {
408 if !voted.contains_key(&relay_account) {
410 ensure!(
412 reward_info
413 .contributed_relay_addresses
414 .contains(&relay_account),
415 Error::<T>::NonContributedAddressProvided
416 );
417
418 ensure!(
421 signature.verify(payload.as_slice(), &relay_account.clone().into()),
422 Error::<T>::InvalidClaimSignature
423 );
424 voted.insert(relay_account, ());
425 }
426 }
427
428 ensure!(
430 Perbill::from_rational(
431 voted.len() as u32,
432 reward_info.contributed_relay_addresses.len() as u32
433 ) >= T::RewardAddressRelayVoteThreshold::get(),
434 Error::<T>::InsufficientNumberOfValidProofs
435 );
436 Ok(())
437 }
438
439 pub fn complete_initialization(
440 lease_ending_block: T::VestingBlockNumber,
441 ) -> DispatchResultWithPostInfo {
442 let initialized = <Initialized<T>>::get();
443
444 ensure!(!initialized, Error::<T>::RewardVecAlreadyInitialized);
446
447 ensure!(
450 lease_ending_block > InitVestingBlock::<T>::get(),
451 Error::<T>::VestingPeriodNonValid
452 );
453
454 let current_initialized_rewards = InitializedRewardAmount::<T>::get();
455
456 let reward_difference = Self::pot().saturating_sub(current_initialized_rewards);
457
458 ensure!(
460 reward_difference < TotalContributors::<T>::get().into(),
461 Error::<T>::RewardsDoNotMatchFund
462 );
463
464 let imbalance = T::RewardCurrency::withdraw(
466 &PALLET_ID.into_account_truncating(),
467 reward_difference,
468 WithdrawReasons::TRANSFER,
469 AllowDeath,
470 )
471 .expect("Shouldnt fail, as the fund should be enough to burn and nothing is locked");
472 drop(imbalance);
473
474 EndVestingBlock::<T>::put(lease_ending_block);
475
476 <Initialized<T>>::put(true);
477
478 Ok(Default::default())
479 }
480
481 pub fn initialize_reward_vec(
482 rewards: Vec<ContributorData<T>>,
483 ) -> DispatchResultWithPostInfo {
484 let initialized = <Initialized<T>>::get();
485 ensure!(!initialized, Error::<T>::RewardVecAlreadyInitialized);
486
487 ensure!(
489 rewards.len() as u32 <= T::MaxInitContributors::get(),
490 Error::<T>::TooManyContributors
491 );
492
493 let mut current_initialized_rewards = InitializedRewardAmount::<T>::get();
495
496 let mut total_contributors = TotalContributors::<T>::get();
498
499 let incoming_rewards: BalanceOf<T> = rewards
500 .iter()
501 .fold(0u32.into(), |acc: BalanceOf<T>, (_, _, reward)| {
502 acc + *reward
503 });
504
505 ensure!(
507 current_initialized_rewards + incoming_rewards <= Self::pot(),
508 Error::<T>::BatchBeyondFundPot
509 );
510
511 for (relay_account, native_account, reward) in &rewards {
512 if ClaimedRelayChainIds::<T>::get(relay_account).is_some()
513 || UnassociatedContributions::<T>::get(relay_account).is_some()
514 {
515 Self::deposit_event(Event::InitializedAlreadyInitializedAccount(
518 relay_account.clone(),
519 native_account.clone(),
520 *reward,
521 ));
522 continue;
523 }
524
525 if *reward < T::MinimumReward::get() {
526 Self::deposit_event(Event::InitializedAccountWithNotEnoughContribution(
529 relay_account.clone(),
530 native_account.clone(),
531 *reward,
532 ));
533 continue;
534 }
535
536 let initial_payment = if let Some(native_account) = native_account {
538 let first_payment = T::InitializationPayment::get() * (*reward);
539 T::RewardCurrency::transfer(
540 &PALLET_ID.into_account_truncating(),
541 native_account,
542 first_payment,
543 AllowDeath,
544 )?;
545 Self::deposit_event(Event::InitialPaymentMade(
546 native_account.clone(),
547 first_payment,
548 ));
549 first_payment
550 } else {
551 0u32.into()
552 };
553
554 let mut reward_info = RewardInfo {
556 total_reward: *reward,
557 claimed_reward: initial_payment,
558 contributed_relay_addresses: vec![relay_account.clone()],
559 };
560
561 current_initialized_rewards += *reward - initial_payment;
562 total_contributors += 1;
563
564 if let Some(native_account) = native_account {
565 if let Some(mut inserted_reward_info) =
566 AccountsPayable::<T>::get(native_account)
567 {
568 inserted_reward_info
569 .contributed_relay_addresses
570 .append(&mut reward_info.contributed_relay_addresses);
571 AccountsPayable::<T>::insert(
573 native_account,
574 RewardInfo {
575 total_reward: inserted_reward_info.total_reward
576 + reward_info.total_reward,
577 claimed_reward: inserted_reward_info.claimed_reward
578 + reward_info.claimed_reward,
579 contributed_relay_addresses: inserted_reward_info
580 .contributed_relay_addresses,
581 },
582 );
583 } else {
584 AccountsPayable::<T>::insert(native_account, reward_info);
586 }
587 ClaimedRelayChainIds::<T>::insert(relay_account, ());
588 } else {
589 UnassociatedContributions::<T>::insert(relay_account, reward_info);
590 }
591 }
592 InitializedRewardAmount::<T>::put(current_initialized_rewards);
593 TotalContributors::<T>::put(total_contributors);
594
595 Ok(Default::default())
596 }
597 }
598
599 #[pallet::error]
600 pub enum Error<T> {
601 AlreadyAssociated,
604 BatchBeyondFundPot,
606 FirstClaimAlreadyDone,
608 RewardNotHighEnough,
610 InvalidClaimSignature,
613 InvalidFreeClaimSignature,
615 NoAssociatedClaim,
619 RewardsAlreadyClaimed,
622 RewardVecAlreadyInitialized,
624 RewardVecNotFullyInitializedYet,
626 RewardsDoNotMatchFund,
628 TooManyContributors,
630 VestingPeriodNonValid,
632 NonContributedAddressProvided,
634 InsufficientNumberOfValidProofs,
636 }
637
638 #[pallet::storage]
639 #[pallet::getter(fn accounts_payable)]
640 pub type AccountsPayable<T: Config> =
641 StorageMap<_, Blake2_128Concat, T::AccountId, RewardInfo<T>>;
642 #[pallet::storage]
643 #[pallet::getter(fn claimed_relay_chain_ids)]
644 pub type ClaimedRelayChainIds<T: Config> =
645 StorageMap<_, Blake2_128Concat, T::RelayChainAccountId, ()>;
646 #[pallet::storage]
647 #[pallet::getter(fn unassociated_contributions)]
648 pub type UnassociatedContributions<T: Config> =
649 StorageMap<_, Blake2_128Concat, T::RelayChainAccountId, RewardInfo<T>>;
650 #[pallet::storage]
651 #[pallet::getter(fn initialized)]
652 pub type Initialized<T: Config> = StorageValue<_, bool, ValueQuery, T::Initialized>;
653
654 #[pallet::storage]
655 #[pallet::storage_prefix = "InitRelayBlock"]
656 #[pallet::getter(fn init_vesting_block)]
657 pub(crate) type InitVestingBlock<T: Config> =
659 StorageValue<_, T::VestingBlockNumber, ValueQuery>;
660
661 #[pallet::storage]
662 #[pallet::storage_prefix = "EndRelayBlock"]
663 #[pallet::getter(fn end_vesting_block)]
664 pub(crate) type EndVestingBlock<T: Config> = StorageValue<_, T::VestingBlockNumber, ValueQuery>;
666
667 #[pallet::storage]
668 #[pallet::getter(fn init_reward_amount)]
669 pub(crate) type InitializedRewardAmount<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
672
673 #[pallet::storage]
674 #[pallet::getter(fn total_contributors)]
675 pub(crate) type TotalContributors<T: Config> = StorageValue<_, u32, ValueQuery>;
677
678 #[pallet::genesis_config]
679 pub struct GenesisConfig<T: Config> {
680 pub funded_accounts: Vec<ContributorData<T>>,
682 pub init_vesting_block: T::VestingBlockNumber,
684 pub end_vesting_block: T::VestingBlockNumber,
686 }
687
688 impl<T: Config> Default for GenesisConfig<T> {
689 fn default() -> Self {
690 Self {
691 funded_accounts: vec![],
692 init_vesting_block: Default::default(),
693 end_vesting_block: Default::default(),
694 }
695 }
696 }
697
698 #[pallet::genesis_build]
699 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
700 fn build(&self) {
701 InitVestingBlock::<T>::put(self.init_vesting_block.clone());
703 EndVestingBlock::<T>::put(self.end_vesting_block.clone());
704
705 let mut total_contributors = 0u32;
706 let mut total_rewards = BalanceOf::<T>::default();
707
708 for (relay_account, native_account_opt, reward) in &self.funded_accounts {
710 if *reward < T::MinimumReward::get() {
712 continue;
713 }
714
715 let initial_payment = if native_account_opt.is_some() {
717 let payment = T::InitializationPayment::get() * (*reward);
718 if let Some(native_account) = native_account_opt {
720 let _ = T::RewardCurrency::transfer(
721 &PALLET_ID.into_account_truncating(),
722 native_account,
723 payment,
724 AllowDeath,
725 );
726 }
727 payment
728 } else {
729 BalanceOf::<T>::default()
730 };
731
732 let reward_info = RewardInfo {
734 total_reward: *reward,
735 claimed_reward: initial_payment,
736 contributed_relay_addresses: vec![relay_account.clone()],
737 };
738
739 if let Some(native_account) = native_account_opt {
741 AccountsPayable::<T>::insert(native_account, &reward_info);
742 ClaimedRelayChainIds::<T>::insert(relay_account, ());
743 } else {
744 UnassociatedContributions::<T>::insert(relay_account, &reward_info);
745 }
746
747 total_contributors += 1;
748 total_rewards += *reward - initial_payment;
749 }
750
751 TotalContributors::<T>::put(total_contributors);
753 InitializedRewardAmount::<T>::put(total_rewards);
754
755 if !self.funded_accounts.is_empty() {
757 <Initialized<T>>::put(true);
758 }
759 }
760 }
761
762 #[pallet::event]
763 #[pallet::generate_deposit(fn deposit_event)]
764 pub enum Event<T: Config> {
765 InitialPaymentMade(T::AccountId, BalanceOf<T>),
767 NativeIdentityAssociated(T::RelayChainAccountId, T::AccountId, BalanceOf<T>),
770 RewardsPaid(T::AccountId, BalanceOf<T>),
773 RewardAddressUpdated(T::AccountId, T::AccountId),
775 InitializedAlreadyInitializedAccount(
777 T::RelayChainAccountId,
778 Option<T::AccountId>,
779 BalanceOf<T>,
780 ),
781 InitializedAccountWithNotEnoughContribution(
783 T::RelayChainAccountId,
784 Option<T::AccountId>,
785 BalanceOf<T>,
786 ),
787 }
788}