#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub use cfg_traits::rewards::{
AccountRewards, CurrencyGroupChange, DistributedRewards, GroupRewards,
};
use frame_support::{
pallet_prelude::*,
traits::{
tokens::{AssetId, Balance},
Time,
},
DefaultNoBound,
};
pub use frame_support::{
storage::{bounded_btree_map::BoundedBTreeMap, transactional},
transactional,
};
use frame_system::pallet_prelude::*;
use num_traits::sign::Unsigned;
pub use pallet::*;
use sp_runtime::{
traits::{EnsureAdd, Zero},
FixedPointOperand,
};
use sp_std::mem;
pub use weights::WeightInfo;
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound)]
#[scale_info(skip_type_params(T))]
pub struct EpochData<T: Config> {
duration: MomentOf<T>,
reward: T::Balance,
weights: BoundedBTreeMap<T::GroupId, T::Weight, T::MaxGroups>,
}
impl<T: Config> Default for EpochData<T> {
fn default() -> Self {
Self {
duration: T::InitialEpochDuration::get(),
reward: T::Balance::zero(),
weights: BoundedBTreeMap::default(),
}
}
}
impl<T: Config> Clone for EpochData<T> {
fn clone(&self) -> Self {
Self {
duration: self.duration,
reward: self.reward,
weights: self.weights.clone(),
}
}
}
#[derive(
PartialEq, Clone, DefaultNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound,
)]
#[scale_info(skip_type_params(T))]
pub struct EpochChanges<T: Config> {
duration: Option<MomentOf<T>>,
reward: Option<T::Balance>,
weights: BoundedBTreeMap<T::GroupId, T::Weight, T::MaxChangesPerEpoch>,
currencies: BoundedBTreeMap<T::CurrencyId, T::GroupId, T::MaxChangesPerEpoch>,
}
pub type MomentOf<T> = <<T as Config>::Timer as Time>::Moment;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[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;
type CurrencyId: AssetId + MaxEncodedLen + Clone + Ord;
type GroupId: Parameter + MaxEncodedLen + Ord + Copy;
type Weight: Parameter + MaxEncodedLen + EnsureAdd + Unsigned + FixedPointOperand + Default;
type Rewards: GroupRewards<Balance = Self::Balance, GroupId = Self::GroupId>
+ AccountRewards<Self::AccountId, Balance = Self::Balance, CurrencyId = Self::CurrencyId>
+ CurrencyGroupChange<GroupId = Self::GroupId, CurrencyId = Self::CurrencyId>
+ DistributedRewards<Balance = Self::Balance, GroupId = Self::GroupId>;
type Timer: Time;
#[pallet::constant]
type MaxGroups: Get<u32> + TypeInfo;
#[pallet::constant]
type MaxChangesPerEpoch: Get<u32> + TypeInfo + sp_std::fmt::Debug + Clone + PartialEq;
#[pallet::constant]
type InitialEpochDuration: Get<MomentOf<Self>>;
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::storage]
pub(super) type EndOfEpoch<T: Config> = StorageValue<_, MomentOf<T>, ValueQuery>;
#[pallet::storage]
pub(super) type ActiveEpochData<T: Config> = StorageValue<_, EpochData<T>, ValueQuery>;
#[pallet::storage]
pub(super) type NextEpochChanges<T: Config> = StorageValue<_, EpochChanges<T>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
NewEpoch {
ends_on: MomentOf<T>,
reward: T::Balance,
last_changes: EpochChanges<T>,
},
}
#[pallet::error]
pub enum Error<T> {
MaxChangesPerEpochReached,
}
#[derive(Default)]
pub struct ChangeCounter {
groups: u32,
weights: u32,
currencies: u32,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
let now = T::Timer::now();
if now < EndOfEpoch::<T>::get() {
return T::DbWeight::get().reads(1);
}
let mut counter = ChangeCounter::default();
transactional::with_storage_layer(|| -> DispatchResult {
let (epoch_data, last_changes) = Self::apply_epoch_changes(&mut counter)?;
let ends_on = now.ensure_add(epoch_data.duration)?;
EndOfEpoch::<T>::set(ends_on);
Self::deposit_event(Event::NewEpoch {
ends_on,
reward: epoch_data.reward,
last_changes,
});
Ok(())
})
.ok();
T::WeightInfo::on_initialize(counter.groups, counter.weights, counter.currencies)
}
}
impl<T: Config> Pallet<T> {
pub fn apply_epoch_changes(
counter: &mut ChangeCounter,
) -> Result<(EpochData<T>, EpochChanges<T>), DispatchError> {
NextEpochChanges::<T>::try_mutate(|changes| {
ActiveEpochData::<T>::try_mutate(|epoch_data| {
counter.groups = T::Rewards::distribute_reward_with_weights(
epoch_data.reward,
epoch_data.weights.iter().map(|(g, w)| (*g, *w)),
)
.map(|results| results.len() as u32)?;
for (&group_id, &weight) in &changes.weights {
epoch_data.weights.try_insert(group_id, weight).ok();
counter.weights += 1;
}
for (currency_id, &group_id) in &changes.currencies.clone() {
T::Rewards::attach_currency(currency_id.clone(), group_id)?;
counter.currencies += 1;
}
epoch_data.reward = changes.reward.unwrap_or(epoch_data.reward);
epoch_data.duration = changes.duration.unwrap_or(epoch_data.duration);
Ok((epoch_data.clone(), mem::take(changes)))
})
})
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(T::WeightInfo::stake())]
#[transactional]
#[pallet::call_index(0)]
pub fn stake(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
amount: T::Balance,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
T::Rewards::deposit_stake(currency_id, &account_id, amount)
}
#[pallet::weight(T::WeightInfo::unstake())]
#[transactional]
#[pallet::call_index(1)]
pub fn unstake(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
amount: T::Balance,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
T::Rewards::withdraw_stake(currency_id, &account_id, amount)
}
#[pallet::weight(T::WeightInfo::claim_reward())]
#[transactional]
#[pallet::call_index(2)]
pub fn claim_reward(origin: OriginFor<T>, currency_id: T::CurrencyId) -> DispatchResult {
let account_id = ensure_signed(origin)?;
T::Rewards::claim_reward(currency_id, &account_id).map(|_| ())
}
#[pallet::weight(T::WeightInfo::set_distributed_reward())]
#[pallet::call_index(3)]
pub fn set_distributed_reward(origin: OriginFor<T>, balance: T::Balance) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
NextEpochChanges::<T>::mutate(|changes| changes.reward = Some(balance));
Ok(())
}
#[pallet::weight(T::WeightInfo::set_epoch_duration())]
#[pallet::call_index(4)]
pub fn set_epoch_duration(origin: OriginFor<T>, duration: MomentOf<T>) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
NextEpochChanges::<T>::mutate(|changes| changes.duration = Some(duration));
Ok(())
}
#[pallet::weight(T::WeightInfo::set_group_weight())]
#[pallet::call_index(5)]
pub fn set_group_weight(
origin: OriginFor<T>,
group_id: T::GroupId,
weight: T::Weight,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
NextEpochChanges::<T>::try_mutate(|changes| {
changes
.weights
.try_insert(group_id, weight)
.map_err(|_| Error::<T>::MaxChangesPerEpochReached)
})?;
Ok(())
}
#[pallet::weight(T::WeightInfo::set_currency_group())]
#[pallet::call_index(6)]
pub fn set_currency_group(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
group_id: T::GroupId,
) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
NextEpochChanges::<T>::try_mutate(|changes| {
changes
.currencies
.try_insert(currency_id, group_id)
.map_err(|_| Error::<T>::MaxChangesPerEpochReached)
})?;
Ok(())
}
}
}