pallet_crowdloan_rewards/
lib.rs

1// Copyright 2019-2022 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # Crowdloan Rewards Pallet
18//!
19//! This pallet is DEPRECATED, the remaining code handle only unclaimed rewards.
20//!
21//! ## Payout Mechanism
22//!
23//! The current payout mechanism requires contributors to claim their payouts. Because they are
24//! paying the transaction fees for this themselves, they can do it as often as every block, or
25//! wait and claim the entire thing once it is fully vested. We could consider auto payouts if we
26//! want.
27
28#![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	// The crowdloan rewards pallet
63	pub struct Pallet<T>(PhantomData<T>);
64
65	pub const PALLET_ID: PalletId = PalletId(*b"Crowdloa");
66
67	// The wrapper around which the reward changing message needs to be wrapped
68	pub const WRAPPED_BYTES_PREFIX: &[u8] = b"<Bytes>";
69	pub const WRAPPED_BYTES_POSTFIX: &[u8] = b"</Bytes>";
70
71	/// Configuration trait of this pallet.
72	#[pallet::config]
73	pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
74		/// Checker for the reward vec, is it initalized already?
75		type Initialized: Get<bool>;
76		/// Percentage to be payed at initialization
77		#[pallet::constant]
78		type InitializationPayment: Get<Perbill>;
79		// Max number of contributors that can be inserted at once in initialize_reward_vec
80		#[pallet::constant]
81		type MaxInitContributors: Get<u32>;
82		/// The minimum contribution to which rewards will be paid.
83		type MinimumReward: Get<BalanceOf<Self>>;
84		/// A fraction representing the percentage of proofs
85		/// that need to be presented to change a reward address through the relay keys
86		#[pallet::constant]
87		type RewardAddressRelayVoteThreshold: Get<Perbill>;
88		/// The currency in which the rewards will be paid (probably the parachain native currency)
89		type RewardCurrency: Currency<Self::AccountId>;
90		/// The AccountId type contributors used on the relay chain.
91		type RelayChainAccountId: Parameter
92			//TODO these AccountId32 bounds feel a little extraneous. I wonder if we can remove them.
93			+ Into<AccountId32>
94			+ From<AccountId32>
95			+ Ord
96			+ sp_runtime::serde::Serialize
97			+ for<'a> sp_runtime::serde::Deserialize<'a>;
98
99		// The origin that is allowed to change the reward address with relay signatures
100		type RewardAddressChangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
101
102		/// Network Identifier to be appended into the signatures for reward address change/association
103		/// Prevents replay attacks from one network to the other
104		#[pallet::constant]
105		type SignatureNetworkIdentifier: Get<&'static [u8]>;
106
107		// The origin that is allowed to change the reward address with relay signatures
108		type RewardAddressAssociateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
109
110		/// The type that will be used to track vesting progress
111		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		/// The notion of time that will be used for vesting. Probably
119		/// either the relay chain or sovereign chain block number.
120		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	/// Type alias for contributor data: (relay_account, optional_native_account, reward)
130	pub type ContributorData<T> = (
131		<T as Config>::RelayChainAccountId,
132		Option<<T as frame_system::Config>::AccountId>,
133		BalanceOf<T>,
134	);
135
136	/// Stores info about the rewards owed as well as how much has been vested so far.
137	/// For a primer on this kind of design, see the recipe on compounding interest
138	/// https://substrate.dev/recipes/fixed-point.html#continuously-compounding
139	#[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		/// Associate a native rewards_destination identity with a crowdloan contribution.
150		///
151		/// The caller needs to provide the unassociated relay account and a proof to succeed
152		/// with the association
153		/// The proof is nothing but a signature over the reward_address using the relay keys
154		#[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			// Check that the origin is the one able to asociate the reward addrss
163			T::RewardAddressChangeOrigin::ensure_origin(origin)?;
164
165			// Check the proof:
166			// 1. Is signed by an actual unassociated contributor
167			// 2. Signs a valid native identity
168			// Check the proof. The Proof consists of a Signature of the rewarded account with the
169			// claimer key
170
171			// The less costly checks will go first
172
173			// The relay account should be unassociated
174			let mut reward_info = UnassociatedContributions::<T>::get(&relay_account)
175				.ok_or(Error::<T>::NoAssociatedClaim)?;
176
177			// We ensure the relay chain id wast not yet associated to avoid multi-claiming
178			// We dont need this right now, as it will always be true if the above check is true
179			ensure!(
180				ClaimedRelayChainIds::<T>::get(&relay_account).is_none(),
181				Error::<T>::AlreadyAssociated
182			);
183
184			// For now I prefer that we dont support providing an existing account here
185			ensure!(
186				AccountsPayable::<T>::get(&reward_account).is_none(),
187				Error::<T>::AlreadyAssociated
188			);
189
190			// b"<Bytes>" "SignatureNetworkIdentifier" + "new_account" + b"</Bytes>"
191			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			// Check the signature
197			Self::verify_signatures(
198				vec![(relay_account.clone(), proof)],
199				reward_info.clone(),
200				payload,
201			)?;
202
203			// Make the first payment
204			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			// Insert on payable
221			AccountsPayable::<T>::insert(&reward_account, &reward_info);
222
223			// Remove from unassociated
224			<UnassociatedContributions<T>>::remove(&relay_account);
225
226			// Insert in mapping
227			ClaimedRelayChainIds::<T>::insert(&relay_account, ());
228
229			// Emit Event
230			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		/// Change reward account by submitting proofs from relay accounts
240		///
241		/// The number of valid proofs needs to be bigger than 'RewardAddressRelayVoteThreshold'
242		/// The account to be changed needs to be submitted as 'previous_account'
243		/// Origin must be RewardAddressChangeOrigin
244		#[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			// Check that the origin is the one able to change the reward addrss
253			T::RewardAddressChangeOrigin::ensure_origin(origin)?;
254
255			// For now I prefer that we dont support providing an existing account here
256			ensure!(
257				AccountsPayable::<T>::get(&reward_account).is_none(),
258				Error::<T>::AlreadyAssociated
259			);
260
261			// To avoid replay attacks, we make sure the payload contains the previous address too
262			// I am assuming no rational user will go back to a previously changed reward address
263			// b"<Bytes>" + "SignatureNetworkIdentifier" + new_account" + "previous_account" + b"</Bytes>"
264			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			// Get the reward info for the account to be changed
271			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			// Remove fromon payable
277			AccountsPayable::<T>::remove(&previous_account);
278
279			// Insert on payable
280			AccountsPayable::<T>::insert(&reward_account, &reward_info);
281
282			// Emit Event
283			Self::deposit_event(Event::RewardAddressUpdated(
284				previous_account,
285				reward_account,
286			));
287
288			Ok(Default::default())
289		}
290
291		/// Collect whatever portion of your reward are currently vested.
292		#[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			// Calculate the veted amount on demand.
299			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			// Get the current block used for vesting purposes
307			let now = T::VestingBlockProvider::current_block_number();
308
309			// Substract the first payment from the vested amount
310			let first_paid = T::InitializationPayment::get() * info.total_reward;
311
312			// To calculate how much could the user have claimed already
313			let payable_period = now.saturating_sub(<InitVestingBlock<T>>::get());
314
315			// How much should the contributor have already claimed by this block?
316			// By multiplying first we allow the conversion to integer done with the biggest number
317			let period = EndVestingBlock::<T>::get() - InitVestingBlock::<T>::get();
318			let should_have_claimed = if period == 0u32.into() {
319				// Pallet is configured with a zero vesting period.
320				info.total_reward - first_paid
321			} else {
322				(info.total_reward - first_paid).saturating_mul(payable_period.into())
323					/ period.into()
324			};
325
326			// If the period is bigger than whats missing to pay, then return whats missing to pay
327			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			// This pallet controls an amount of funds and transfers them to each of the contributors
337			//TODO: contributors should have the balance locked for transfers but not for democracy
338			T::RewardCurrency::transfer(
339				&PALLET_ID.into_account_truncating(),
340				&payee,
341				payable_amount,
342				AllowDeath,
343			)?;
344			// Emit event
345			Self::deposit_event(Event::RewardsPaid(payee, payable_amount));
346			Ok(Default::default())
347		}
348
349		/// Update reward address, proving that the caller owns the current native key
350		#[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			// Calculate the veted amount on demand.
359			let info = AccountsPayable::<T>::get(&signer).ok_or(Error::<T>::NoAssociatedClaim)?;
360
361			// For now I prefer that we dont support providing an existing account here
362			ensure!(
363				AccountsPayable::<T>::get(&new_reward_account).is_none(),
364				Error::<T>::AlreadyAssociated
365			);
366
367			// Remove previous rewarded account
368			AccountsPayable::<T>::remove(&signer);
369
370			// Update new rewarded account
371			AccountsPayable::<T>::insert(&new_reward_account, &info);
372
373			// Emit event
374			Self::deposit_event(Event::RewardAddressUpdated(signer, new_reward_account));
375
376			Ok(Default::default())
377		}
378	}
379
380	impl<T: Config> Pallet<T> {
381		/// The account ID that holds the Crowdloan's funds
382		pub fn account_id() -> T::AccountId {
383			PALLET_ID.into_account_truncating()
384		}
385		/// The Account Id's balance
386		pub fn pot() -> BalanceOf<T> {
387			T::RewardCurrency::free_balance(&Self::account_id())
388		}
389		/// Verify a set of signatures made with relay chain accounts
390		/// We are verifying all the signatures, and then counting
391		/// We could do something more efficient like count as we verify
392		/// In any of the cases the weight will need to account for all the signatures,
393		/// as we dont know beforehand whether they will be valid
394		#[allow(clippy::map_entry)] // Cannot use entry API due to ensure! checks before insert
395		fn verify_signatures(
396			proofs: Vec<(T::RelayChainAccountId, MultiSignature)>,
397			reward_info: RewardInfo<T>,
398			payload: Vec<u8>,
399		) -> DispatchResult {
400			// The proofs should
401			// 1. be signed by contributors to this address, otherwise they are not counted
402			// 2. Signs a valid native identity
403			// 3. The sum of the valid proofs needs to be bigger than InsufficientNumberOfValidProofs
404
405			// I use a map here for faster lookups
406			let mut voted: BTreeMap<T::RelayChainAccountId, ()> = BTreeMap::new();
407			for (relay_account, signature) in proofs {
408				// We just count votes that we have not seen
409				if !voted.contains_key(&relay_account) {
410					// Maybe I should not error here?
411					ensure!(
412						reward_info
413							.contributed_relay_addresses
414							.contains(&relay_account),
415						Error::<T>::NonContributedAddressProvided
416					);
417
418					// I am erroring here as I think it is good to know the reason in the single-case
419					// signature
420					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 the votes are sufficient
429			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			// This ensures there was no prior initialization
445			ensure!(!initialized, Error::<T>::RewardVecAlreadyInitialized);
446
447			// This ensures the end vesting block (when all funds are fully vested)
448			// is bigger than the init vesting block
449			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 the difference is not bigger than the total number of contributors
459			ensure!(
460				reward_difference < TotalContributors::<T>::get().into(),
461				Error::<T>::RewardsDoNotMatchFund
462			);
463
464			// Burn the difference
465			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 we are below the max number of contributors
488			ensure!(
489				rewards.len() as u32 <= T::MaxInitContributors::get(),
490				Error::<T>::TooManyContributors
491			);
492
493			// What is the amount initialized so far?
494			let mut current_initialized_rewards = InitializedRewardAmount::<T>::get();
495
496			// Total number of contributors
497			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 we dont go over funds
506			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					// Dont fail as this is supposed to be called with batch calls and we
516					// dont want to stall the rest of the contributions
517					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					// Don't fail as this is supposed to be called with batch calls and we
527					// dont want to stall the rest of the contributions
528					Self::deposit_event(Event::InitializedAccountWithNotEnoughContribution(
529						relay_account.clone(),
530						native_account.clone(),
531						*reward,
532					));
533					continue;
534				}
535
536				// If we have a native_account, we make the payment
537				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				// Calculate the reward info to store after the initial payment has been made.
555				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						// the native account has already some rewards in, we add the new ones
572						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						// First reward association
585						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		/// User trying to associate a native identity with a relay chain identity for posterior
602		/// reward claiming provided an already associated relay chain identity
603		AlreadyAssociated,
604		/// Trying to introduce a batch that goes beyond the limits of the funds
605		BatchBeyondFundPot,
606		/// First claim already done
607		FirstClaimAlreadyDone,
608		/// The contribution is not high enough to be eligible for rewards
609		RewardNotHighEnough,
610		/// User trying to associate a native identity with a relay chain identity for posterior
611		/// reward claiming provided a wrong signature
612		InvalidClaimSignature,
613		/// User trying to claim the first free reward provided the wrong signature
614		InvalidFreeClaimSignature,
615		/// User trying to claim an award did not have an claim associated with it. This may mean
616		/// they did not contribute to the crowdloan, or they have not yet associated a native id
617		/// with their contribution
618		NoAssociatedClaim,
619		/// User trying to claim rewards has already claimed all rewards associated with its
620		/// identity and contribution
621		RewardsAlreadyClaimed,
622		/// Reward vec has already been initialized
623		RewardVecAlreadyInitialized,
624		/// Reward vec has not yet been fully initialized
625		RewardVecNotFullyInitializedYet,
626		/// Rewards should match funds of the pallet
627		RewardsDoNotMatchFund,
628		/// Initialize_reward_vec received too many contributors
629		TooManyContributors,
630		/// Provided vesting period is not valid
631		VestingPeriodNonValid,
632		/// User provided a signature from a non-contributor relay account
633		NonContributedAddressProvided,
634		/// User submitted an unsifficient number of proofs to change the reward address
635		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	/// Vesting block height at the initialization of the pallet
658	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	/// Vesting block height at the initialization of the pallet
665	pub(crate) type EndVestingBlock<T: Config> = StorageValue<_, T::VestingBlockNumber, ValueQuery>;
666
667	#[pallet::storage]
668	#[pallet::getter(fn init_reward_amount)]
669	/// Total initialized amount so far. We store this to make pallet funds == contributors reward
670	/// check easier and more efficient
671	pub(crate) type InitializedRewardAmount<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
672
673	#[pallet::storage]
674	#[pallet::getter(fn total_contributors)]
675	/// Total number of contributors to aid hinting benchmarking
676	pub(crate) type TotalContributors<T: Config> = StorageValue<_, u32, ValueQuery>;
677
678	#[pallet::genesis_config]
679	pub struct GenesisConfig<T: Config> {
680		/// List of contributors with their relay account, optional native account, and reward amount
681		pub funded_accounts: Vec<ContributorData<T>>,
682		/// Initial vesting block number
683		pub init_vesting_block: T::VestingBlockNumber,
684		/// End vesting block number
685		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			// Set the vesting blocks
702			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			// Process each funded account
709			for (relay_account, native_account_opt, reward) in &self.funded_accounts {
710				// Skip if reward is less than minimum
711				if *reward < T::MinimumReward::get() {
712					continue;
713				}
714
715				// Calculate the initial payment
716				let initial_payment = if native_account_opt.is_some() {
717					let payment = T::InitializationPayment::get() * (*reward);
718					// Transfer the initial payment to the native account
719					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				// Create reward info
733				let reward_info = RewardInfo {
734					total_reward: *reward,
735					claimed_reward: initial_payment,
736					contributed_relay_addresses: vec![relay_account.clone()],
737				};
738
739				// Store the reward info based on whether account is associated
740				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			// Update total contributors and initialized reward amount
752			TotalContributors::<T>::put(total_contributors);
753			InitializedRewardAmount::<T>::put(total_rewards);
754
755			// Mark as initialized only if there are funded accounts
756			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		/// The initial payment of InitializationPayment % was paid
766		InitialPaymentMade(T::AccountId, BalanceOf<T>),
767		/// Someone has proven they made a contribution and associated a native identity with it.
768		/// Data is the relay account,  native account and the total amount of _rewards_ that will be paid
769		NativeIdentityAssociated(T::RelayChainAccountId, T::AccountId, BalanceOf<T>),
770		/// A contributor has claimed some rewards.
771		/// Data is the account getting paid and the amount of rewards paid.
772		RewardsPaid(T::AccountId, BalanceOf<T>),
773		/// A contributor has updated the reward address.
774		RewardAddressUpdated(T::AccountId, T::AccountId),
775		/// When initializing the reward vec an already initialized account was found
776		InitializedAlreadyInitializedAccount(
777			T::RelayChainAccountId,
778			Option<T::AccountId>,
779			BalanceOf<T>,
780		),
781		/// When initializing the reward vec an already initialized account was found
782		InitializedAccountWithNotEnoughContribution(
783			T::RelayChainAccountId,
784			Option<T::AccountId>,
785			BalanceOf<T>,
786		),
787	}
788}