#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod tests;
use codec::{Decode, Encode};
use frame_support::weights::Weight;
use frame_support::{
ensure,
pallet_prelude::MaxEncodedLen,
traits::{tokens::ExistenceRequirement, Contains, Currency, Get},
BoundedVec, PalletId,
};
use frame_system::ensure_signed;
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::TypeInfo;
use sp_arithmetic::traits::UniqueSaturatedInto;
use sp_runtime::{
traits::{AccountIdConversion, BlockNumberProvider, Bounded, CheckedAdd, CheckedDiv, One, Saturating, Zero},
DispatchResult, Perbill, RuntimeDebug,
};
use sp_std::prelude::*;
use support::WithAccountId;
pub mod weights;
pub use weights::WeightInfo;
pub use pallet::*;
type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
#[derive(Default, Encode, Decode, MaxEncodedLen, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
enum Releases {
#[default]
V0, V1, }
#[derive(Default, TypeInfo)]
pub struct MintCurve<T: Config> {
session_period: BlockNumberFor<T>,
fiscal_period: BlockNumberFor<T>,
inflation_steps: Vec<Perbill>,
maximum_supply: BalanceOf<T>,
}
impl<T: Config> MintCurve<T> {
pub fn new(
session_period: BlockNumberFor<T>,
fiscal_period: BlockNumberFor<T>,
inflation_steps: &[Perbill],
maximum_supply: BalanceOf<T>,
) -> Self {
let valid_session_period = session_period.max(One::one());
let valid_fiscal_period = fiscal_period.max(valid_session_period);
Self {
session_period: valid_session_period,
fiscal_period: valid_fiscal_period,
inflation_steps: inflation_steps.to_vec(),
maximum_supply,
}
}
pub fn calc_session_quota(
&self,
n: BlockNumberFor<T>,
curve_start: BlockNumberFor<T>,
current_supply: BalanceOf<T>,
) -> BalanceOf<T> {
let step: usize = n
.saturating_sub(curve_start)
.checked_div(&self.fiscal_period)
.unwrap_or_else(Bounded::max_value)
.unique_saturated_into();
let max_inflation_rate = *self
.inflation_steps
.get(step)
.or_else(|| self.inflation_steps.last())
.unwrap_or(&Zero::zero());
let target_increase =
(self.maximum_supply.saturating_sub(current_supply)).min(max_inflation_rate * current_supply);
Perbill::from_rational(self.session_period, self.fiscal_period) * target_increase
}
pub fn next_quota_renew_schedule(&self, n: BlockNumberFor<T>, curve_start: BlockNumberFor<T>) -> BlockNumberFor<T> {
Self::next_schedule(n, curve_start, self.session_period)
}
pub fn next_quota_calc_schedule(&self, n: BlockNumberFor<T>, curve_start: BlockNumberFor<T>) -> BlockNumberFor<T> {
Self::next_schedule(n, curve_start, self.fiscal_period)
}
#[inline(always)]
pub fn session_period(&self) -> BlockNumberFor<T> {
self.session_period
}
#[inline(always)]
pub fn fiscal_period(&self) -> BlockNumberFor<T> {
self.fiscal_period
}
#[inline(always)]
pub fn maximum_supply(&self) -> BalanceOf<T> {
self.maximum_supply
}
fn next_schedule(
n: BlockNumberFor<T>,
curve_start: BlockNumberFor<T>,
period: BlockNumberFor<T>,
) -> BlockNumberFor<T> {
if n >= curve_start {
n.saturating_sub(curve_start)
.checked_div(&period)
.unwrap_or_else(Bounded::max_value)
.saturating_add(One::one())
.saturating_mul(period)
.saturating_add(curve_start)
} else {
let schedule = curve_start.saturating_sub(
curve_start
.saturating_sub(n)
.checked_div(&period)
.unwrap_or_else(Bounded::max_value)
.saturating_mul(period),
);
if n == schedule {
n.saturating_add(period)
} else {
schedule
}
}
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::dispatch::PostDispatchInfo;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Currency: Currency<Self::AccountId>;
type PalletId: Get<PalletId>;
#[pallet::constant]
type ProtocolFee: Get<Perbill>;
type ProtocolFeeReceiver: WithAccountId<Self::AccountId>;
#[pallet::constant]
type ExistentialDeposit: Get<BalanceOf<Self>>;
#[pallet::constant]
type MaxAllocs: Get<u32>;
type OracleMembers: Contains<Self::AccountId>;
type MintCurve: Get<&'static MintCurve<Self>>;
type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::allocate(batch.len().try_into().unwrap_or_else(|_| T::MaxAllocs::get())).saturating_add(T::WeightInfo::checked_update_session_quota()))]
pub fn batch(
origin: OriginFor<T>,
batch: BoundedVec<(T::AccountId, BalanceOf<T>), T::MaxAllocs>,
) -> DispatchResultWithPostInfo {
Self::ensure_oracle(origin)?;
let update_weight = Self::checked_update_session_quota();
let rewards_len = batch.len().try_into().unwrap_or_else(|_| T::MaxAllocs::get());
Self::allocate(batch)?;
let dispatch_info = PostDispatchInfo::from((
Some(update_weight.saturating_add(T::WeightInfo::allocate(rewards_len))),
Pays::No,
));
Ok(dispatch_info)
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::set_curve_starting_block())]
pub fn set_curve_starting_block(
origin: OriginFor<T>,
curve_start: BlockNumberFor<T>,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
<MintCurveStartingBlock<T>>::put(curve_start);
Self::update_session_quota_schedules(curve_start);
Ok(Pays::No.into())
}
}
#[pallet::error]
pub enum Error<T> {
OracleAccessDenied,
AllocationExceedsSessionQuota,
DoesNotSatisfyExistentialDeposit,
BatchEmpty,
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
SessionQuotaRenewed,
SessionQuotaCalculated(BalanceOf<T>),
}
#[pallet::storage]
pub(crate) type StorageVersion<T: Config> = StorageValue<_, Releases, ValueQuery>;
#[cfg(feature = "runtime-benchmarks")]
#[pallet::storage]
#[pallet::getter(fn benchmark_oracles)]
pub type BenchmarkOracles<T: Config> =
StorageValue<_, BoundedVec<T::AccountId, benchmarking::MaxMembers>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn session_quota)]
pub(crate) type SessionQuota<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn next_session_quota)]
pub(crate) type NextSessionQuota<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn quota_renew_schedule)]
pub(crate) type SessionQuotaRenewSchedule<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn quota_calc_schedule)]
pub(crate) type SessionQuotaCalculationSchedule<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn mint_curve_starting_block)]
pub(crate) type MintCurveStartingBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
}
impl<T: Config> Pallet<T> {
pub fn is_oracle(who: T::AccountId) -> bool {
#[cfg(feature = "runtime-benchmarks")]
return T::OracleMembers::contains(&who) || Self::benchmark_oracles().contains(&who);
#[cfg(not(feature = "runtime-benchmarks"))]
return T::OracleMembers::contains(&who);
}
fn ensure_oracle(origin: T::RuntimeOrigin) -> DispatchResult {
let sender = ensure_signed(origin)?;
ensure!(Self::is_oracle(sender), Error::<T>::OracleAccessDenied);
Ok(())
}
fn allocate(batch: BoundedVec<(T::AccountId, BalanceOf<T>), T::MaxAllocs>) -> DispatchResult {
ensure!(batch.len() > Zero::zero(), Error::<T>::BatchEmpty);
let min_alloc = T::ExistentialDeposit::get().saturating_mul(2u32.into());
let mut full_issuance: BalanceOf<T> = Zero::zero();
for (_account, amount) in batch.iter() {
ensure!(amount >= &min_alloc, Error::<T>::DoesNotSatisfyExistentialDeposit,);
full_issuance = full_issuance
.checked_add(amount)
.ok_or(Error::<T>::AllocationExceedsSessionQuota)?;
}
let session_quota = <SessionQuota<T>>::get();
ensure!(
full_issuance <= session_quota,
Error::<T>::AllocationExceedsSessionQuota
);
<SessionQuota<T>>::put(session_quota.saturating_sub(full_issuance));
T::Currency::resolve_creating(
&T::PalletId::get().into_account_truncating(),
T::Currency::issue(full_issuance),
);
let mut full_protocol: BalanceOf<T> = Zero::zero();
for (account, amount) in batch.iter().cloned() {
let amount_for_protocol = T::ProtocolFee::get() * amount;
let amount_for_grantee = amount.saturating_sub(amount_for_protocol);
T::Currency::transfer(
&T::PalletId::get().into_account_truncating(),
&account,
amount_for_grantee,
ExistenceRequirement::KeepAlive,
)?;
full_protocol = full_protocol.saturating_add(amount_for_protocol);
}
T::Currency::transfer(
&T::PalletId::get().into_account_truncating(),
&T::ProtocolFeeReceiver::account_id(),
full_protocol,
ExistenceRequirement::AllowDeath,
)?;
Ok(())
}
fn checked_update_session_quota() -> Weight {
let n = T::BlockNumberProvider::current_block_number();
let read_block_number_weight = T::DbWeight::get().reads(1);
let calc_quota_weight = Self::checked_calc_session_quota(n);
let renew_quota_weight = Self::checked_renew_session_quota(n);
read_block_number_weight
.saturating_add(calc_quota_weight)
.saturating_add(renew_quota_weight)
}
fn update_session_quota_schedules(curve_start: BlockNumberFor<T>) {
let n = T::BlockNumberProvider::current_block_number();
Self::update_session_quota_calculation_schedule(n, curve_start);
Self::update_session_quota_renew_schedule(n, curve_start);
}
fn checked_calc_session_quota(n: BlockNumberFor<T>) -> Weight {
if n >= <SessionQuotaCalculationSchedule<T>>::get() {
let curve_start = Self::curve_start_or(n);
Self::update_session_quota_calculation_schedule(n, curve_start);
let session_quota = T::MintCurve::get().calc_session_quota(n, curve_start, T::Currency::total_issuance());
<NextSessionQuota<T>>::put(session_quota);
Self::deposit_event(Event::SessionQuotaCalculated(session_quota));
T::WeightInfo::calc_quota()
} else {
T::DbWeight::get().reads(1)
}
}
fn checked_renew_session_quota(n: BlockNumberFor<T>) -> Weight {
if n >= <SessionQuotaRenewSchedule<T>>::get() {
let curve_start = Self::curve_start_or(n);
Self::update_session_quota_renew_schedule(n, curve_start);
<SessionQuota<T>>::put(<NextSessionQuota<T>>::get());
Self::deposit_event(Event::SessionQuotaRenewed);
T::WeightInfo::renew_quota()
} else {
T::DbWeight::get().reads(1)
}
}
fn update_session_quota_calculation_schedule(n: BlockNumberFor<T>, curve_start: BlockNumberFor<T>) {
let next_schedule = T::MintCurve::get().next_quota_calc_schedule(n, curve_start);
<SessionQuotaCalculationSchedule<T>>::put(next_schedule);
}
fn update_session_quota_renew_schedule(n: BlockNumberFor<T>, curve_start: BlockNumberFor<T>) {
let next_schedule = T::MintCurve::get().next_quota_renew_schedule(n, curve_start);
<SessionQuotaRenewSchedule<T>>::put(next_schedule);
}
fn curve_start_or(n: BlockNumberFor<T>) -> BlockNumberFor<T> {
<MintCurveStartingBlock<T>>::get().unwrap_or_else(|| {
<MintCurveStartingBlock<T>>::put(n);
n
})
}
}