Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: limit allocations to targeted supply inflation #667

Merged
merged 22 commits into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4a2a25a
feat: limit allocations to targeted supply inflation [WIP]
aliXsed Sep 1, 2022
d02ec42
fix: enforce session period value before fiscal period value
aliXsed Sep 8, 2022
eeab0cf
refactor: enforce only a valid mint_curve can be constructed
aliXsed Sep 10, 2022
7285cf7
fix: configure the inflation steps for the allocations' pallet in Ede…
aliXsed Sep 11, 2022
6968572
feat: remove error for exceeding maximum supply
aliXsed Sep 11, 2022
9bc2a94
feat: emit events on a session renewal and on a fiscal calculation
aliXsed Sep 11, 2022
dc2d93b
Merge branch 'master' into aliX/allocations-throttle
aliXsed Sep 11, 2022
9de6457
build: fix merge to use sp_arithmatic
aliXsed Sep 11, 2022
568b3db
feat: benchmark on_initialize
aliXsed Sep 12, 2022
93048b8
test: verify benchmark for on_initialize
aliXsed Sep 12, 2022
8b30654
perf: use inline when appropriate
aliXsed Sep 12, 2022
85ab03a
refactor: remove unnecessary static lifetime
aliXsed Sep 12, 2022
a2c8707
build: fix clippy warnings
aliXsed Sep 14, 2022
bf3ff64
feat: configure eden's mint curve out of the target curve taken from …
aliXsed Sep 14, 2022
11f9b6d
Merge branch 'master' into aliX/allocations-throttle
aliXsed Sep 14, 2022
7d9dae4
fix: only allow building valid curves
aliXsed Sep 15, 2022
9ebd6e1
fix: benchmarking to calculate finer and more accurate weight for on_…
aliXsed Sep 16, 2022
0727407
feat: use relay chain block number as the source of time
aliXsed Sep 16, 2022
38b8dad
fix: base the mint curve config upon the relay chain block number (Po…
aliXsed Sep 16, 2022
b430029
feat: make the starting block of the curve configurable with ROOT per…
aliXsed Sep 18, 2022
784a4a5
test: improve code coverage
aliXsed Sep 18, 2022
738ca0c
chore: use 365/4 = 91.25 as the duration of the fiscal period
aliXsed Sep 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pallets/allocations/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ pallet-balances = { git = "https://github.com/paritytech/substrate", default-fea
sp-io = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
sp-arithmetic = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
support = { path = "../../support" }

[dev-dependencies]
sp-tracing = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
pallet-membership = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.28" }
lazy_static = {version = "1.4.0", default-features = false, features = ["spin_no_std"] }
35 changes: 33 additions & 2 deletions pallets/allocations/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

use super::*;
use crate::BalanceOf;
#[cfg(test)]
use crate::Pallet as Allocations;
use frame_benchmarking::{account, benchmarks};
use frame_support::{traits::ConstU32, BoundedVec};
Expand All @@ -32,17 +31,26 @@ use sp_std::prelude::*;
pub type MaxMembers = ConstU32<10>;

const SEED: u32 = 0;
const ALLOC_FACTOR: u32 = 10;

fn make_batch<T: Config>(b: u32) -> BoundedVec<(T::AccountId, BalanceOf<T>), T::MaxAllocs> {
let mut ret = BoundedVec::with_bounded_capacity(b as usize);

for i in 0..b {
let account = account("grantee", i, SEED);
let _ = ret.try_push((account, T::ExistentialDeposit::get() * 10u32.into()));
let _ = ret.try_push((account, T::ExistentialDeposit::get() * ALLOC_FACTOR.into()));
}
ret
}

fn assert_last_event<T: Config>(generic_event: <T as Config>::Event) {
let events = frame_system::Pallet::<T>::events();
assert!(!events.is_empty());
let system_event: <T as frame_system::Config>::Event = generic_event.into();
let frame_system::EventRecord { event, .. } = &events[events.len() - 1];
aliXsed marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(event, &system_event);
}

benchmarks! {
batch {
let b in 1..T::MaxAllocs::get();
Expand All @@ -52,8 +60,31 @@ benchmarks! {
let mut members = <BenchmarkOracles<T>>::get();
assert!(members.try_push(oracle.clone()).is_ok());
<BenchmarkOracles<T>>::put(&members);
<SessionQuota<T>>::put(T::ExistentialDeposit::get() * (b * ALLOC_FACTOR).into());
}: _(RawOrigin::Signed(oracle), batch_arg)

calc_quota {
}: {
Allocations::<T>::checked_calc_session_quota(Zero::zero(), true);
}
verify {
assert_last_event::<T>(Event::SessionQuotaCalculated(<NextSessionQuota<T>>::get().unwrap()).into())
}

renew_quota {
}: {
Allocations::<T>::checked_renew_session_quota(Zero::zero(), true);
}
verify {
assert_last_event::<T>(Event::SessionQuotaRenewed.into())
}

set_curve_starting_block {
}: _(RawOrigin::Root, One::one())
verify {
assert_eq!(<MintCurveStartingBlock<T>>::get(), Some(One::one()));
}

impl_benchmark_test_suite!(
Allocations,
crate::tests::new_test_ext(),
Expand Down
191 changes: 180 additions & 11 deletions pallets/allocations/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ use frame_support::{
ensure,
pallet_prelude::MaxEncodedLen,
traits::{tokens::ExistenceRequirement, Contains, Currency, Get},
weights::Weight,
BoundedVec, PalletId,
};

use frame_system::ensure_signed;
use scale_info::TypeInfo;
use sp_runtime::traits::AccountIdConversion;
use sp_arithmetic::traits::{CheckedRem, UniqueSaturatedInto};
use sp_runtime::{
traits::{CheckedAdd, Saturating, Zero},
traits::{AccountIdConversion, BlockNumberProvider, CheckedAdd, CheckedDiv, One, Saturating, Zero},
DispatchResult, Perbill, RuntimeDebug,
};
use sp_std::prelude::*;
Expand Down Expand Up @@ -65,6 +66,75 @@ impl Default for Releases {
}
}

#[derive(Default, TypeInfo)]
pub struct MintCurve<T: Config> {
session_period: T::BlockNumber,
fiscal_period: T::BlockNumber,
inflation_steps: Vec<Perbill>,
maximum_supply: BalanceOf<T>,
}

impl<T: Config> MintCurve<T> {
pub fn new(
session_period: T::BlockNumber,
fiscal_period: T::BlockNumber,
inflation_steps: &[Perbill],
maximum_supply: BalanceOf<T>,
) -> Self {
let valid_session_period = session_period.max(One::one());
Self {
// Enforce a session period is at least one block
session_period: valid_session_period,
// Enforce a fiscal period is greater or equal a session period
fiscal_period: fiscal_period.max(valid_session_period),
inflation_steps: inflation_steps.to_vec(),
maximum_supply,
}
}

pub fn checked_calc_next_session_quota(
&self,
block_number: T::BlockNumber,
current_supply: BalanceOf<T>,
forced: bool,
) -> Option<BalanceOf<T>> {
if (block_number.checked_rem(&self.fiscal_period) == Some(T::BlockNumber::zero())) || forced {
let step: usize = block_number
.checked_div(&self.fiscal_period)
.unwrap_or_else(Zero::zero)
.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);
let session_quota = Perbill::from_rational(self.session_period, self.fiscal_period) * target_increase;
Some(session_quota)
aliXsed marked this conversation as resolved.
Show resolved Hide resolved
} else {
None
}
}

pub fn should_update_session_quota(&self, block_number: T::BlockNumber) -> bool {
block_number.checked_rem(&self.session_period) == Some(T::BlockNumber::zero())
}

#[inline(always)]
pub fn session_period(&self) -> T::BlockNumber {
self.session_period
}
#[inline(always)]
pub fn fiscal_period(&self) -> T::BlockNumber {
self.fiscal_period
}
#[inline(always)]
pub fn maximum_supply(&self) -> BalanceOf<T> {
self.maximum_supply
}
}

#[frame_support::pallet]
pub mod pallet {
use super::*;
Expand All @@ -73,6 +143,8 @@ pub mod pallet {

#[pallet::config]
pub trait Config: frame_system::Config {
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;

type Currency: Currency<Self::AccountId>;

type PalletId: Get<PalletId>;
Expand All @@ -81,9 +153,6 @@ pub mod pallet {
type ProtocolFee: Get<Perbill>;
type ProtocolFeeReceiver: WithAccountId<Self::AccountId>;

#[pallet::constant]
type MaximumSupply: Get<BalanceOf<Self>>;

/// Runtime existential deposit
#[pallet::constant]
type ExistentialDeposit: Get<BalanceOf<Self>>;
Expand All @@ -94,6 +163,15 @@ pub mod pallet {

type OracleMembers: Contains<Self::AccountId>;

/// MintCurve acts as an upper bound limiting how much the total token issuance can inflate
/// over a configured session
type MintCurve: Get<&'static MintCurve<Self>>;

/// Provide access to the block number that should be used in mint curve calculations. For
/// example those who use this pallet for a parachain may decide to use the block creation
/// pace of the relay chain for timing.
type BlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;

/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
Expand All @@ -102,6 +180,19 @@ pub mod pallet {
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(PhantomData<T>);

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
let n = Self::relative_block_number();
let forced = <NextSessionQuota<T>>::get().is_none();
Self::checked_calc_session_quota(n, forced)
.saturating_add(Self::checked_renew_session_quota(n, forced))
// Storage: Allocations NextSessionQuota (r:1 w:0)
// Storage: Allocations MintCurveStartingBlock (r:1 w:0)
.saturating_add(T::DbWeight::get().reads(2 as Weight))
}
}

#[pallet::call]
impl<T: Config> Pallet<T> {
/// Optimized allocation call, which will batch allocations of various amounts
Expand All @@ -125,15 +216,17 @@ pub mod pallet {
// overflow, so too many coins to allocate
full_issuance = full_issuance
.checked_add(amount)
.ok_or(Error::<T>::TooManyCoinsToAllocate)?;
.ok_or(Error::<T>::AllocationExceedsSessionQuota)?;
}

let current_supply = T::Currency::total_issuance();
let session_quota = <SessionQuota<T>>::get();
ensure!(
current_supply.saturating_add(full_issuance) <= T::MaximumSupply::get(),
Error::<T>::TooManyCoinsToAllocate
full_issuance <= session_quota,
Error::<T>::AllocationExceedsSessionQuota
);

<SessionQuota<T>>::put(session_quota.saturating_sub(full_issuance));

// allocate the coins to the proxy account
T::Currency::resolve_creating(
&T::PalletId::get().into_account_truncating(),
Expand Down Expand Up @@ -164,20 +257,36 @@ pub mod pallet {

Ok(Pays::No.into())
}

#[pallet::weight(T::WeightInfo::set_curve_starting_block())]
pub fn set_curve_starting_block(origin: OriginFor<T>, n: T::BlockNumber) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
<MintCurveStartingBlock<T>>::put(n);
Ok(Pays::No.into())
}
}

#[pallet::error]
pub enum Error<T> {
/// Function is restricted to oracles only
OracleAccessDenied,
/// We are trying to allocate more coins than we can
TooManyCoinsToAllocate,
/// We are exceeding the session's limit for rewards
AllocationExceedsSessionQuota,
/// Amount is too low and will conflict with the ExistentialDeposit parameter
DoesNotSatisfyExistentialDeposit,
/// Batch is empty or no issuance is necessary
BatchEmpty,
}

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Session quota is renewed at the beginning of a new session
SessionQuotaRenewed,
/// Session quota is calculated and this new value will be used from the next session
SessionQuotaCalculated(BalanceOf<T>),
}

#[cfg(not(tarpaulin))]
#[pallet::storage]
pub(crate) type StorageVersion<T: Config> = StorageValue<_, Releases, ValueQuery>;
Expand All @@ -187,6 +296,27 @@ pub mod pallet {
#[pallet::getter(fn benchmark_oracles)]
pub type BenchmarkOracles<T: Config> =
StorageValue<_, BoundedVec<T::AccountId, benchmarking::MaxMembers>, ValueQuery>;

/// The transitional allocation quota that is left for the current session.
///
/// This will be renewed on a new allocation session
#[pallet::storage]
#[pallet::getter(fn session_quota)]
pub(crate) type SessionQuota<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;

/// The next session's allocation quota, in other words, the top up that is coming for
/// `SessionQuota`.
///
/// The next session quota is calculated from the targeted max inflation rates for the current
/// fiscal period and is renewed on a new fiscal period.
#[pallet::storage]
#[pallet::getter(fn next_session_quota)]
pub(crate) type NextSessionQuota<T: Config> = StorageValue<_, BalanceOf<T>, OptionQuery>;

/// The block from which the mint curve should be considered starting its first inflation step
#[pallet::storage]
#[pallet::getter(fn mint_curve_starting_block)]
pub(crate) type MintCurveStartingBlock<T: Config> = StorageValue<_, T::BlockNumber, OptionQuery>;
}

impl<T: Config> Pallet<T> {
Expand All @@ -202,4 +332,43 @@ impl<T: Config> Pallet<T> {
ensure!(Self::is_oracle(sender), Error::<T>::OracleAccessDenied);
Ok(())
}

/// Calculate the session quota and update the corresponding storage only at the beginning of a
/// fiscal period.
/// Return the weight of itself.
fn checked_calc_session_quota(n: T::BlockNumber, forced: bool) -> Weight {
if let Some(session_quota) =
T::MintCurve::get().checked_calc_next_session_quota(n, T::Currency::total_issuance(), forced)
{
<NextSessionQuota<T>>::put(session_quota);
Self::deposit_event(Event::SessionQuotaCalculated(session_quota));
T::WeightInfo::calc_quota()
} else {
T::DbWeight::get().reads(1 as Weight) // Storage: Balances TotalIssuance (r:1 w:0)
}
}

/// Renew the session quota from the calculated value only at the beginning of a session period.
/// Return the weight of itself.
fn checked_renew_session_quota(n: T::BlockNumber, forced: bool) -> Weight {
if T::MintCurve::get().should_update_session_quota(n) || forced {
<SessionQuota<T>>::put(<NextSessionQuota<T>>::get().unwrap_or_else(Zero::zero));
Self::deposit_event(Event::SessionQuotaRenewed);
T::WeightInfo::renew_quota()
} else {
0
}
}

/// Update the mint curve starting block from the current block number if it's not initialised
/// yet.
/// Return the current block number relative to the starting block of the mint curve.
fn relative_block_number() -> T::BlockNumber {
let n = T::BlockNumberProvider::current_block_number();
let curve_start = <MintCurveStartingBlock<T>>::get().unwrap_or_else(|| {
<MintCurveStartingBlock<T>>::put(n);
n
});
n.saturating_sub(curve_start)
}
}