#![cfg_attr(not(feature = "std"), no_std)]
pub use crate::weights::WeightInfo;
use frame_support::pallet;
pub use pallet::*;
#[cfg(any(test, feature = "runtime-benchmarks"))]
mod benchmarks;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
#[pallet]
pub mod pallet {
use super::*;
use frame_support::{
pallet_prelude::*,
traits::{Currency, ExistenceRequirement::AllowDeath, WithdrawReasons},
PalletId,
};
use frame_system::pallet_prelude::*;
use parity_scale_codec::{Decode, Encode};
use sp_core::crypto::AccountId32;
use sp_runtime::traits::{
AccountIdConversion, AtLeast32BitUnsigned, BlockNumberProvider, Saturating, Verify,
};
use sp_runtime::{MultiSignature, Perbill};
use sp_std::collections::btree_map::BTreeMap;
use sp_std::vec;
use sp_std::vec::Vec;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(PhantomData<T>);
pub const PALLET_ID: PalletId = PalletId(*b"Crowdloa");
pub const WRAPPED_BYTES_PREFIX: &[u8] = b"<Bytes>";
pub const WRAPPED_BYTES_POSTFIX: &[u8] = b"</Bytes>";
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Initialized: Get<bool>;
#[pallet::constant]
type InitializationPayment: Get<Perbill>;
#[pallet::constant]
type MaxInitContributors: Get<u32>;
type MinimumReward: Get<BalanceOf<Self>>;
#[pallet::constant]
type RewardAddressRelayVoteThreshold: Get<Perbill>;
type RewardCurrency: Currency<Self::AccountId>;
type RelayChainAccountId: Parameter
+ Into<AccountId32>
+ From<AccountId32>
+ Ord
+ sp_runtime::serde::Serialize
+ for<'a> sp_runtime::serde::Deserialize<'a>;
type RewardAddressChangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
#[pallet::constant]
type SignatureNetworkIdentifier: Get<&'static [u8]>;
type RewardAddressAssociateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type VestingBlockNumber: AtLeast32BitUnsigned
+ Parameter
+ Default
+ Into<BalanceOf<Self>>
+ sp_runtime::serde::Serialize
+ for<'a> sp_runtime::serde::Deserialize<'a>;
type VestingBlockProvider: BlockNumberProvider<BlockNumber = Self::VestingBlockNumber>;
type WeightInfo: WeightInfo;
}
pub type BalanceOf<T> = <<T as Config>::RewardCurrency as Currency<
<T as frame_system::Config>::AccountId,
>>::Balance;
pub type ContributorData<T> = (
<T as Config>::RelayChainAccountId,
Option<<T as frame_system::Config>::AccountId>,
BalanceOf<T>,
);
#[derive(Default, Clone, Encode, Decode, RuntimeDebug, PartialEq, scale_info::TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct RewardInfo<T: Config> {
pub total_reward: BalanceOf<T>,
pub claimed_reward: BalanceOf<T>,
pub contributed_relay_addresses: Vec<T::RelayChainAccountId>,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::associate_native_identity())]
pub fn associate_native_identity(
origin: OriginFor<T>,
reward_account: T::AccountId,
relay_account: T::RelayChainAccountId,
proof: MultiSignature,
) -> DispatchResultWithPostInfo {
T::RewardAddressChangeOrigin::ensure_origin(origin)?;
let mut reward_info = UnassociatedContributions::<T>::get(&relay_account)
.ok_or(Error::<T>::NoAssociatedClaim)?;
ensure!(
ClaimedRelayChainIds::<T>::get(&relay_account).is_none(),
Error::<T>::AlreadyAssociated
);
ensure!(
AccountsPayable::<T>::get(&reward_account).is_none(),
Error::<T>::AlreadyAssociated
);
let mut payload = WRAPPED_BYTES_PREFIX.to_vec();
payload.append(&mut T::SignatureNetworkIdentifier::get().to_vec());
payload.append(&mut reward_account.encode());
payload.append(&mut WRAPPED_BYTES_POSTFIX.to_vec());
Self::verify_signatures(
vec![(relay_account.clone(), proof)],
reward_info.clone(),
payload,
)?;
let first_payment = T::InitializationPayment::get() * reward_info.total_reward;
T::RewardCurrency::transfer(
&PALLET_ID.into_account_truncating(),
&reward_account,
first_payment,
AllowDeath,
)?;
Self::deposit_event(Event::InitialPaymentMade(
reward_account.clone(),
first_payment,
));
reward_info.claimed_reward = first_payment;
AccountsPayable::<T>::insert(&reward_account, &reward_info);
<UnassociatedContributions<T>>::remove(&relay_account);
ClaimedRelayChainIds::<T>::insert(&relay_account, ());
Self::deposit_event(Event::NativeIdentityAssociated(
relay_account,
reward_account,
reward_info.total_reward,
));
Ok(Default::default())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::change_association_with_relay_keys(proofs.len() as u32))]
pub fn change_association_with_relay_keys(
origin: OriginFor<T>,
reward_account: T::AccountId,
previous_account: T::AccountId,
proofs: Vec<(T::RelayChainAccountId, MultiSignature)>,
) -> DispatchResultWithPostInfo {
T::RewardAddressChangeOrigin::ensure_origin(origin)?;
ensure!(
AccountsPayable::<T>::get(&reward_account).is_none(),
Error::<T>::AlreadyAssociated
);
let mut payload = WRAPPED_BYTES_PREFIX.to_vec();
payload.append(&mut T::SignatureNetworkIdentifier::get().to_vec());
payload.append(&mut reward_account.encode());
payload.append(&mut previous_account.encode());
payload.append(&mut WRAPPED_BYTES_POSTFIX.to_vec());
let reward_info = AccountsPayable::<T>::get(&previous_account)
.ok_or(Error::<T>::NoAssociatedClaim)?;
Self::verify_signatures(proofs, reward_info.clone(), payload)?;
AccountsPayable::<T>::remove(&previous_account);
AccountsPayable::<T>::insert(&reward_account, &reward_info);
Self::deposit_event(Event::RewardAddressUpdated(
previous_account,
reward_account,
));
Ok(Default::default())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::claim())]
pub fn claim(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let payee = ensure_signed(origin)?;
let initialized = <Initialized<T>>::get();
ensure!(initialized, Error::<T>::RewardVecNotFullyInitializedYet);
let mut info =
AccountsPayable::<T>::get(&payee).ok_or(Error::<T>::NoAssociatedClaim)?;
ensure!(
info.claimed_reward < info.total_reward,
Error::<T>::RewardsAlreadyClaimed
);
let now = T::VestingBlockProvider::current_block_number();
let first_paid = T::InitializationPayment::get() * info.total_reward;
let payable_period = now.saturating_sub(<InitVestingBlock<T>>::get());
let period = EndVestingBlock::<T>::get() - InitVestingBlock::<T>::get();
let should_have_claimed = if period == 0u32.into() {
info.total_reward - first_paid
} else {
(info.total_reward - first_paid).saturating_mul(payable_period.into())
/ period.into()
};
let payable_amount = if should_have_claimed >= (info.total_reward - first_paid) {
info.total_reward.saturating_sub(info.claimed_reward)
} else {
should_have_claimed + first_paid - info.claimed_reward
};
info.claimed_reward = info.claimed_reward.saturating_add(payable_amount);
AccountsPayable::<T>::insert(&payee, &info);
T::RewardCurrency::transfer(
&PALLET_ID.into_account_truncating(),
&payee,
payable_amount,
AllowDeath,
)?;
Self::deposit_event(Event::RewardsPaid(payee, payable_amount));
Ok(Default::default())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::update_reward_address())]
pub fn update_reward_address(
origin: OriginFor<T>,
new_reward_account: T::AccountId,
) -> DispatchResultWithPostInfo {
let signer = ensure_signed(origin)?;
let info = AccountsPayable::<T>::get(&signer).ok_or(Error::<T>::NoAssociatedClaim)?;
ensure!(
AccountsPayable::<T>::get(&new_reward_account).is_none(),
Error::<T>::AlreadyAssociated
);
AccountsPayable::<T>::remove(&signer);
AccountsPayable::<T>::insert(&new_reward_account, &info);
Self::deposit_event(Event::RewardAddressUpdated(signer, new_reward_account));
Ok(Default::default())
}
}
impl<T: Config> Pallet<T> {
pub fn account_id() -> T::AccountId {
PALLET_ID.into_account_truncating()
}
pub fn pot() -> BalanceOf<T> {
T::RewardCurrency::free_balance(&Self::account_id())
}
#[allow(clippy::map_entry)] fn verify_signatures(
proofs: Vec<(T::RelayChainAccountId, MultiSignature)>,
reward_info: RewardInfo<T>,
payload: Vec<u8>,
) -> DispatchResult {
let mut voted: BTreeMap<T::RelayChainAccountId, ()> = BTreeMap::new();
for (relay_account, signature) in proofs {
if !voted.contains_key(&relay_account) {
ensure!(
reward_info
.contributed_relay_addresses
.contains(&relay_account),
Error::<T>::NonContributedAddressProvided
);
ensure!(
signature.verify(payload.as_slice(), &relay_account.clone().into()),
Error::<T>::InvalidClaimSignature
);
voted.insert(relay_account, ());
}
}
ensure!(
Perbill::from_rational(
voted.len() as u32,
reward_info.contributed_relay_addresses.len() as u32
) >= T::RewardAddressRelayVoteThreshold::get(),
Error::<T>::InsufficientNumberOfValidProofs
);
Ok(())
}
pub fn complete_initialization(
lease_ending_block: T::VestingBlockNumber,
) -> DispatchResultWithPostInfo {
let initialized = <Initialized<T>>::get();
ensure!(!initialized, Error::<T>::RewardVecAlreadyInitialized);
ensure!(
lease_ending_block > InitVestingBlock::<T>::get(),
Error::<T>::VestingPeriodNonValid
);
let current_initialized_rewards = InitializedRewardAmount::<T>::get();
let reward_difference = Self::pot().saturating_sub(current_initialized_rewards);
ensure!(
reward_difference < TotalContributors::<T>::get().into(),
Error::<T>::RewardsDoNotMatchFund
);
let imbalance = T::RewardCurrency::withdraw(
&PALLET_ID.into_account_truncating(),
reward_difference,
WithdrawReasons::TRANSFER,
AllowDeath,
)
.expect("Shouldnt fail, as the fund should be enough to burn and nothing is locked");
drop(imbalance);
EndVestingBlock::<T>::put(lease_ending_block);
<Initialized<T>>::put(true);
Ok(Default::default())
}
pub fn initialize_reward_vec(
rewards: Vec<ContributorData<T>>,
) -> DispatchResultWithPostInfo {
let initialized = <Initialized<T>>::get();
ensure!(!initialized, Error::<T>::RewardVecAlreadyInitialized);
ensure!(
rewards.len() as u32 <= T::MaxInitContributors::get(),
Error::<T>::TooManyContributors
);
let mut current_initialized_rewards = InitializedRewardAmount::<T>::get();
let mut total_contributors = TotalContributors::<T>::get();
let incoming_rewards: BalanceOf<T> = rewards
.iter()
.fold(0u32.into(), |acc: BalanceOf<T>, (_, _, reward)| {
acc + *reward
});
ensure!(
current_initialized_rewards + incoming_rewards <= Self::pot(),
Error::<T>::BatchBeyondFundPot
);
for (relay_account, native_account, reward) in &rewards {
if ClaimedRelayChainIds::<T>::get(relay_account).is_some()
|| UnassociatedContributions::<T>::get(relay_account).is_some()
{
Self::deposit_event(Event::InitializedAlreadyInitializedAccount(
relay_account.clone(),
native_account.clone(),
*reward,
));
continue;
}
if *reward < T::MinimumReward::get() {
Self::deposit_event(Event::InitializedAccountWithNotEnoughContribution(
relay_account.clone(),
native_account.clone(),
*reward,
));
continue;
}
let initial_payment = if let Some(native_account) = native_account {
let first_payment = T::InitializationPayment::get() * (*reward);
T::RewardCurrency::transfer(
&PALLET_ID.into_account_truncating(),
native_account,
first_payment,
AllowDeath,
)?;
Self::deposit_event(Event::InitialPaymentMade(
native_account.clone(),
first_payment,
));
first_payment
} else {
0u32.into()
};
let mut reward_info = RewardInfo {
total_reward: *reward,
claimed_reward: initial_payment,
contributed_relay_addresses: vec![relay_account.clone()],
};
current_initialized_rewards += *reward - initial_payment;
total_contributors += 1;
if let Some(native_account) = native_account {
if let Some(mut inserted_reward_info) =
AccountsPayable::<T>::get(native_account)
{
inserted_reward_info
.contributed_relay_addresses
.append(&mut reward_info.contributed_relay_addresses);
AccountsPayable::<T>::insert(
native_account,
RewardInfo {
total_reward: inserted_reward_info.total_reward
+ reward_info.total_reward,
claimed_reward: inserted_reward_info.claimed_reward
+ reward_info.claimed_reward,
contributed_relay_addresses: inserted_reward_info
.contributed_relay_addresses,
},
);
} else {
AccountsPayable::<T>::insert(native_account, reward_info);
}
ClaimedRelayChainIds::<T>::insert(relay_account, ());
} else {
UnassociatedContributions::<T>::insert(relay_account, reward_info);
}
}
InitializedRewardAmount::<T>::put(current_initialized_rewards);
TotalContributors::<T>::put(total_contributors);
Ok(Default::default())
}
}
#[pallet::error]
pub enum Error<T> {
AlreadyAssociated,
BatchBeyondFundPot,
FirstClaimAlreadyDone,
RewardNotHighEnough,
InvalidClaimSignature,
InvalidFreeClaimSignature,
NoAssociatedClaim,
RewardsAlreadyClaimed,
RewardVecAlreadyInitialized,
RewardVecNotFullyInitializedYet,
RewardsDoNotMatchFund,
TooManyContributors,
VestingPeriodNonValid,
NonContributedAddressProvided,
InsufficientNumberOfValidProofs,
}
#[pallet::storage]
#[pallet::getter(fn accounts_payable)]
pub type AccountsPayable<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, RewardInfo<T>>;
#[pallet::storage]
#[pallet::getter(fn claimed_relay_chain_ids)]
pub type ClaimedRelayChainIds<T: Config> =
StorageMap<_, Blake2_128Concat, T::RelayChainAccountId, ()>;
#[pallet::storage]
#[pallet::getter(fn unassociated_contributions)]
pub type UnassociatedContributions<T: Config> =
StorageMap<_, Blake2_128Concat, T::RelayChainAccountId, RewardInfo<T>>;
#[pallet::storage]
#[pallet::getter(fn initialized)]
pub type Initialized<T: Config> = StorageValue<_, bool, ValueQuery, T::Initialized>;
#[pallet::storage]
#[pallet::storage_prefix = "InitRelayBlock"]
#[pallet::getter(fn init_vesting_block)]
pub(crate) type InitVestingBlock<T: Config> =
StorageValue<_, T::VestingBlockNumber, ValueQuery>;
#[pallet::storage]
#[pallet::storage_prefix = "EndRelayBlock"]
#[pallet::getter(fn end_vesting_block)]
pub(crate) type EndVestingBlock<T: Config> = StorageValue<_, T::VestingBlockNumber, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn init_reward_amount)]
pub(crate) type InitializedRewardAmount<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn total_contributors)]
pub(crate) type TotalContributors<T: Config> = StorageValue<_, u32, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub funded_accounts: Vec<ContributorData<T>>,
pub init_vesting_block: T::VestingBlockNumber,
pub end_vesting_block: T::VestingBlockNumber,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
funded_accounts: vec![],
init_vesting_block: Default::default(),
end_vesting_block: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
InitVestingBlock::<T>::put(self.init_vesting_block.clone());
EndVestingBlock::<T>::put(self.end_vesting_block.clone());
let mut total_contributors = 0u32;
let mut total_rewards = BalanceOf::<T>::default();
for (relay_account, native_account_opt, reward) in &self.funded_accounts {
if *reward < T::MinimumReward::get() {
continue;
}
let initial_payment = if native_account_opt.is_some() {
let payment = T::InitializationPayment::get() * (*reward);
if let Some(native_account) = native_account_opt {
let _ = T::RewardCurrency::transfer(
&PALLET_ID.into_account_truncating(),
native_account,
payment,
AllowDeath,
);
}
payment
} else {
BalanceOf::<T>::default()
};
let reward_info = RewardInfo {
total_reward: *reward,
claimed_reward: initial_payment,
contributed_relay_addresses: vec![relay_account.clone()],
};
if let Some(native_account) = native_account_opt {
AccountsPayable::<T>::insert(native_account, &reward_info);
ClaimedRelayChainIds::<T>::insert(relay_account, ());
} else {
UnassociatedContributions::<T>::insert(relay_account, &reward_info);
}
total_contributors += 1;
total_rewards += *reward - initial_payment;
}
TotalContributors::<T>::put(total_contributors);
InitializedRewardAmount::<T>::put(total_rewards);
if !self.funded_accounts.is_empty() {
<Initialized<T>>::put(true);
}
}
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
InitialPaymentMade(T::AccountId, BalanceOf<T>),
NativeIdentityAssociated(T::RelayChainAccountId, T::AccountId, BalanceOf<T>),
RewardsPaid(T::AccountId, BalanceOf<T>),
RewardAddressUpdated(T::AccountId, T::AccountId),
InitializedAlreadyInitializedAccount(
T::RelayChainAccountId,
Option<T::AccountId>,
BalanceOf<T>,
),
InitializedAccountWithNotEnoughContribution(
T::RelayChainAccountId,
Option<T::AccountId>,
BalanceOf<T>,
),
}
}