#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod migrations;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
use cfg_traits::{
self,
rewards::{AccountRewards, CurrencyGroupChange, GroupRewards},
Seconds, TimeAsSecs,
};
use cfg_types::fixed_point::FixedPointNumberExtension;
use frame_support::{
pallet_prelude::*,
storage::transactional,
traits::{
fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate},
fungibles::{Inspect, Mutate},
tokens::{Balance, Fortitude, Precision},
OneSessionHandler,
},
DefaultNoBound, PalletId,
};
use frame_system::pallet_prelude::*;
use num_traits::sign::Unsigned;
pub use pallet::*;
use sp_runtime::{
traits::{AccountIdConversion, EnsureAdd, EnsureMul, Zero},
FixedPointNumber, FixedPointOperand, SaturatedConversion, Saturating,
};
use sp_std::{mem, vec::Vec};
pub use weights::WeightInfo;
#[derive(
Encode, Decode, DefaultNoBound, Clone, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebugNoBound,
)]
#[scale_info(skip_type_params(T))]
pub struct CollatorChanges<T: Config> {
pub inc: BoundedVec<T::AccountId, T::MaxCollators>,
pub out: BoundedVec<T::AccountId, T::MaxCollators>,
}
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Eq, RuntimeDebugNoBound)]
#[scale_info(skip_type_params(T))]
pub struct SessionData<T: Config> {
pub(crate) collator_reward: T::Balance,
pub collator_count: u32,
pub(crate) treasury_inflation_rate: T::Rate,
pub(crate) last_update: Seconds,
}
impl<T: Config> Default for SessionData<T> {
fn default() -> Self {
Self {
collator_count: 0,
collator_reward: T::Balance::zero(),
treasury_inflation_rate: T::Rate::zero(),
last_update: Seconds::zero(),
}
}
}
#[derive(
PartialEq, Clone, DefaultNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound,
)]
#[scale_info(skip_type_params(T))]
pub struct SessionChanges<T: Config> {
pub collators: CollatorChanges<T>,
pub collator_count: Option<u32>,
pub collator_reward: Option<T::Balance>,
treasury_inflation_rate: Option<T::Rate>,
last_update: Seconds,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type Balance: Balance + MaxEncodedLen + FixedPointOperand + MaybeSerializeDeserialize;
type Weight: Parameter + MaxEncodedLen + EnsureAdd + Unsigned + FixedPointOperand + Default;
type Rewards: GroupRewards<Balance = Self::Balance, GroupId = u32>
+ AccountRewards<
Self::AccountId,
Balance = Self::Balance,
CurrencyId = <Self as Config>::CurrencyId,
> + CurrencyGroupChange<GroupId = u32, CurrencyId = <Self as Config>::CurrencyId>;
type Tokens: Mutate<Self::AccountId>
+ Inspect<
Self::AccountId,
AssetId = <Self as Config>::CurrencyId,
Balance = Self::Balance,
> + FungibleMutate<Self::AccountId>
+ FungibleInspect<Self::AccountId, Balance = Self::Balance>;
type CurrencyId: Parameter + Member + Copy + Ord + MaxEncodedLen;
#[pallet::constant]
type StakeCurrencyId: Get<<Self as Config>::CurrencyId>;
#[pallet::constant]
type StakeAmount: Get<<Self as Config>::Balance>;
#[pallet::constant]
type StakeGroupId: Get<u32>;
#[pallet::constant]
type MaxCollators: Get<u32> + TypeInfo + sp_std::fmt::Debug + Clone + PartialEq;
type TreasuryPalletId: Get<PalletId>;
type AuthorityId: Member
+ Parameter
+ sp_runtime::RuntimeAppPublic
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type Rate: Parameter
+ Member
+ FixedPointNumberExtension
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type Time: TimeAsSecs;
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn active_session_data)]
pub(super) type ActiveSessionData<T: Config> = StorageValue<_, SessionData<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn next_session_changes)]
pub(super) type NextSessionChanges<T: Config> = StorageValue<_, SessionChanges<T>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
NewSession {
collator_reward: T::Balance,
treasury_inflation_rate: T::Rate,
last_changes: SessionChanges<T>,
},
SessionAdvancementFailed {
error: DispatchError,
},
}
#[pallet::error]
pub enum Error<T> {}
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub collators: Vec<T::AccountId>,
pub collator_reward: T::Balance,
pub treasury_inflation_rate: T::Rate,
pub last_update: Seconds,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
T::Rewards::attach_currency(T::StakeCurrencyId::get(), T::StakeGroupId::get()).expect(
"Should be able to attach default block rewards staking currency to collator group",
);
ActiveSessionData::<T>::mutate(|session_data| {
session_data.collator_count = self.collators.len().saturated_into();
session_data.collator_reward = self.collator_reward;
session_data.treasury_inflation_rate = self.treasury_inflation_rate;
session_data.last_update = self.last_update;
});
for collator in &self.collators {
Pallet::<T>::do_init_collator(collator)
.expect("Should not panic when initiating genesis collators for block rewards");
}
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(T::WeightInfo::claim_reward())]
#[pallet::call_index(0)]
pub fn claim_reward(origin: OriginFor<T>, account_id: T::AccountId) -> DispatchResult {
ensure_signed(origin)?;
T::Rewards::claim_reward(T::StakeCurrencyId::get(), &account_id).map(|_| ())
}
#[pallet::weight(T::WeightInfo::set_collator_reward_per_session())]
#[pallet::call_index(1)]
pub fn set_collator_reward_per_session(
origin: OriginFor<T>,
collator_reward_per_session: T::Balance,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
NextSessionChanges::<T>::mutate(|c| {
c.collator_reward = Some(collator_reward_per_session);
});
Ok(())
}
#[pallet::weight(T::WeightInfo::set_annual_treasury_inflation_rate())]
#[pallet::call_index(2)]
pub fn set_annual_treasury_inflation_rate(
origin: OriginFor<T>,
treasury_inflation_rate: T::Rate,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
NextSessionChanges::<T>::mutate(|c| {
c.treasury_inflation_rate = Some(treasury_inflation_rate);
});
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
pub(crate) fn do_init_collator(who: &T::AccountId) -> DispatchResult {
let existential_deposit =
<T::Tokens as Inspect<T::AccountId>>::minimum_balance(T::StakeCurrencyId::get());
<T::Tokens as Mutate<T::AccountId>>::mint_into(
T::StakeCurrencyId::get(),
who,
T::StakeAmount::get().saturating_add(existential_deposit),
)?;
T::Rewards::deposit_stake(T::StakeCurrencyId::get(), who, T::StakeAmount::get())
}
pub(crate) fn do_exit_collator(who: &T::AccountId) -> DispatchResult {
let amount = T::Rewards::account_stake(T::StakeCurrencyId::get(), who);
T::Rewards::withdraw_stake(T::StakeCurrencyId::get(), who, amount)?;
<T::Tokens as Mutate<T::AccountId>>::burn_from(
T::StakeCurrencyId::get(),
who,
amount,
Precision::Exact,
Fortitude::Polite,
)
.map(|_| ())
}
pub(crate) fn calculate_epoch_treasury_inflation(
annual_inflation_rate: T::Rate,
last_update: Seconds,
) -> T::Balance {
let total_issuance = <T::Tokens as FungibleInspect<T::AccountId>>::total_issuance();
let session_duration = T::Time::now().saturating_sub(last_update);
let inflation_proration =
cfg_types::pools::saturated_rate_proration(annual_inflation_rate, session_duration);
inflation_proration.saturating_mul_int(total_issuance)
}
fn do_advance_session() {
let mut num_joining = 0u32;
let mut num_leaving = 0u32;
transactional::with_storage_layer(|| -> DispatchResult {
NextSessionChanges::<T>::try_mutate(|changes| -> DispatchResult {
ActiveSessionData::<T>::try_mutate(|session_data| {
let total_collator_reward = session_data
.collator_reward
.ensure_mul(session_data.collator_count.into())?;
T::Rewards::reward_group(T::StakeGroupId::get(), total_collator_reward)?;
let treasury_inflation = Self::calculate_epoch_treasury_inflation(
session_data.treasury_inflation_rate,
session_data.last_update,
);
if !treasury_inflation.is_zero() {
let _ = <T::Tokens as FungibleMutate<T::AccountId>>::mint_into(
&T::TreasuryPalletId::get().into_account_truncating(),
treasury_inflation,
)
.map_err(|e| {
log::error!(
"Failed to mint treasury inflation for session due to error {:?}",
e
)
});
}
num_joining = changes.collators.inc.len().saturated_into();
num_leaving = changes.collators.out.len().saturated_into();
for leaving in changes.collators.out.iter() {
Self::do_exit_collator(leaving)?;
}
for joining in changes.collators.inc.iter() {
Self::do_init_collator(joining)?;
}
session_data.collator_reward = changes
.collator_reward
.unwrap_or(session_data.collator_reward);
session_data.treasury_inflation_rate = changes
.treasury_inflation_rate
.unwrap_or(session_data.treasury_inflation_rate);
session_data.collator_count = changes
.collator_count
.unwrap_or(session_data.collator_count);
session_data.last_update = T::Time::now();
Self::deposit_event(Event::NewSession {
collator_reward: session_data.collator_reward,
treasury_inflation_rate: session_data.treasury_inflation_rate,
last_changes: mem::take(changes),
});
Ok(())
})
})
})
.map_err(|error| {
Self::deposit_event(Event::SessionAdvancementFailed { error });
})
.ok();
}
}
impl<T: Config> sp_runtime::BoundToRuntimeAppPublic for Pallet<T> {
type Public = T::AuthorityId;
}
impl<T: Config> OneSessionHandler<T::AccountId> for Pallet<T> {
type Key = T::AuthorityId;
fn on_genesis_session<'a, I: 'a>(_validators: I)
where
I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
{
}
fn on_new_session<'a, I: 'a>(_: bool, validators: I, queued_validators: I)
where
I: Iterator<Item = (&'a T::AccountId, T::AuthorityId)>,
{
Self::do_advance_session();
let current = validators
.map(|(acc_id, _)| acc_id.clone())
.collect::<Vec<_>>();
let next = queued_validators
.map(|(acc_id, _)| acc_id.clone())
.collect::<Vec<_>>();
if current != next {
NextSessionChanges::<T>::mutate(
|SessionChanges {
collators,
collator_count,
..
}| {
let inc = next
.clone()
.into_iter()
.filter(|n| !current.iter().any(|curr| curr == n))
.collect::<Vec<_>>();
let out = current
.clone()
.into_iter()
.filter(|curr| !next.iter().any(|n| n == curr))
.collect::<Vec<_>>();
collators.inc = BoundedVec::<_, T::MaxCollators>::truncate_from(inc);
collators.out = BoundedVec::<_, T::MaxCollators>::truncate_from(out);
*collator_count = Some(next.len().saturated_into::<u32>());
},
);
}
}
fn on_before_session_ending() {
}
fn on_disabled(_validator_index: u32) {
}
}