#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode};
use frame_support::{
pallet_prelude::*,
traits::{tokens::currency::MultiTokenCurrency, Get, Imbalance, MultiTokenVestingSchedule},
};
use frame_system::pallet_prelude::*;
use mangata_support::traits::{ComputeIssuance, GetIssuance, LiquidityMiningApi};
use orml_tokens::MultiTokenCurrencyExtended;
use scale_info::TypeInfo;
use sp_runtime::{
traits::{CheckedAdd, CheckedDiv, CheckedSub, One, Saturating, Zero},
Perbill, Percent, RuntimeDebug,
};
use sp_std::{convert::TryInto, prelude::*};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod benchmarking;
#[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq, Eq, TypeInfo)]
pub struct IssuanceInfo<Balance> {
pub cap: Balance,
pub issuance_at_init: Balance,
pub linear_issuance_blocks: u32,
pub liquidity_mining_split: Perbill,
pub staking_split: Perbill,
pub total_crowdloan_allocation: Balance,
}
#[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq, Eq, TypeInfo)]
pub struct TgeInfo<AccountId, Balance> {
pub who: AccountId,
pub amount: Balance,
}
pub trait WeightInfo {
fn init_issuance_config() -> Weight;
fn finalize_tge() -> Weight;
fn execute_tge(x: u32) -> Weight;
}
impl WeightInfo for () {
fn init_issuance_config() -> Weight {
Weight::from_parts(50_642_000, 0)
}
fn finalize_tge() -> Weight {
Weight::from_parts(50_830_000, 0)
}
fn execute_tge(l: u32) -> Weight {
Weight::from_parts(52_151_000, 0)
.saturating_add((Weight::from_parts(130_000, 0)).saturating_mul(l as u64))
}
}
pub use pallet::*;
type BalanceOf<T> =
<<T as Config>::Tokens as MultiTokenCurrency<<T as frame_system::Config>::AccountId>>::Balance;
type CurrencyIdOf<T> = <<T as Config>::Tokens as MultiTokenCurrency<
<T as frame_system::Config>::AccountId,
>>::CurrencyId;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type NativeCurrencyId: Get<CurrencyIdOf<Self>>;
type Tokens: MultiTokenCurrencyExtended<Self::AccountId>;
#[pallet::constant]
type BlocksPerRound: Get<u32>;
#[pallet::constant]
type HistoryLimit: Get<u32>;
#[pallet::constant]
type LiquidityMiningIssuanceVault: Get<Self::AccountId>;
#[pallet::constant]
type StakingIssuanceVault: Get<Self::AccountId>;
#[pallet::constant]
type TotalCrowdloanAllocation: Get<BalanceOf<Self>>;
#[pallet::constant]
type ImmediateTGEReleasePercent: Get<Percent>;
#[pallet::constant]
type IssuanceCap: Get<BalanceOf<Self>>;
#[pallet::constant]
type LinearIssuanceBlocks: Get<u32>;
#[pallet::constant]
type LiquidityMiningSplit: Get<Perbill>;
#[pallet::constant]
type StakingSplit: Get<Perbill>;
#[pallet::constant]
type TGEReleasePeriod: Get<u32>;
#[pallet::constant]
type TGEReleaseBegin: Get<u32>;
type VestingProvider: MultiTokenVestingSchedule<
Self::AccountId,
Currency = Self::Tokens,
Moment = BlockNumberFor<Self>,
>;
type WeightInfo: WeightInfo;
type LiquidityMiningApi: LiquidityMiningApi<BalanceOf<Self>>;
}
#[pallet::storage]
#[pallet::getter(fn get_issuance_config)]
pub type IssuanceConfigStore<T: Config> =
StorageValue<_, IssuanceInfo<BalanceOf<T>>, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn get_tge_total)]
pub type TGETotal<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn is_tge_finalized)]
pub type IsTGEFinalized<T: Config> = StorageValue<_, bool, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn get_session_issuance)]
pub type SessionIssuance<T: Config> =
StorageMap<_, Twox64Concat, u32, Option<(BalanceOf<T>, BalanceOf<T>)>, ValueQuery>;
#[pallet::error]
pub enum Error<T> {
IssuanceConfigAlreadyInitialized,
IssuanceConfigNotInitialized,
TGENotFinalized,
TGEIsAlreadyFinalized,
IssuanceConfigInvalid,
MathError,
UnknownPool,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::init_issuance_config())]
pub fn init_issuance_config(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
Self::do_init_issuance_config()
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::finalize_tge())]
pub fn finalize_tge(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
ensure!(!IsTGEFinalized::<T>::get(), Error::<T>::TGEIsAlreadyFinalized);
IsTGEFinalized::<T>::put(true);
Pallet::<T>::deposit_event(Event::TGEFinalized);
Ok(().into())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::execute_tge(tge_infos.len() as u32))]
pub fn execute_tge(
origin: OriginFor<T>,
tge_infos: Vec<TgeInfo<T::AccountId, BalanceOf<T>>>,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;
ensure!(!IsTGEFinalized::<T>::get(), Error::<T>::TGEIsAlreadyFinalized);
ensure!(!T::TGEReleasePeriod::get().is_zero(), Error::<T>::MathError);
let lock_percent: Percent = Percent::from_percent(100)
.checked_sub(&T::ImmediateTGEReleasePercent::get())
.ok_or(Error::<T>::MathError)?;
for tge_info in tge_infos {
let locked: BalanceOf<T> = (lock_percent * tge_info.amount).max(One::one());
let per_block: BalanceOf<T> =
(locked / T::TGEReleasePeriod::get().into()).max(One::one());
if T::VestingProvider::can_add_vesting_schedule(
&tge_info.who,
locked,
per_block,
T::TGEReleaseBegin::get().into(),
T::NativeCurrencyId::get().into(),
)
.is_ok()
{
let imb = T::Tokens::deposit_creating(
T::NativeCurrencyId::get().into(),
&tge_info.who.clone(),
tge_info.amount,
);
if !tge_info.amount.is_zero() && imb.peek().is_zero() {
Pallet::<T>::deposit_event(Event::TGEInstanceFailed(tge_info));
} else {
let _ = T::VestingProvider::add_vesting_schedule(
&tge_info.who,
locked,
per_block,
T::TGEReleaseBegin::get().into(),
T::NativeCurrencyId::get().into(),
);
TGETotal::<T>::mutate(|v| *v = v.saturating_add(tge_info.amount));
Pallet::<T>::deposit_event(Event::TGEInstanceSucceeded(tge_info));
}
} else {
Pallet::<T>::deposit_event(Event::TGEInstanceFailed(tge_info));
}
}
Ok(().into())
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
SessionIssuanceIssued(u32, BalanceOf<T>, BalanceOf<T>),
SessionIssuanceRecorded(u32, BalanceOf<T>, BalanceOf<T>),
IssuanceConfigInitialized(IssuanceInfo<BalanceOf<T>>),
TGEFinalized,
TGEInstanceFailed(TgeInfo<T::AccountId, BalanceOf<T>>),
TGEInstanceSucceeded(TgeInfo<T::AccountId, BalanceOf<T>>),
}
}
impl<T: Config> ComputeIssuance for Pallet<T> {
fn initialize() {
IsTGEFinalized::<T>::put(true);
Self::do_init_issuance_config().unwrap();
}
fn compute_issuance(n: u32) {
let _ = Pallet::<T>::calculate_and_store_round_issuance(n);
let _ = Pallet::<T>::clear_round_issuance_history(n);
}
}
pub trait ProvideTotalCrowdloanRewardAllocation<T: Config> {
fn get_total_crowdloan_allocation() -> Option<BalanceOf<T>>;
}
impl<T: Config> ProvideTotalCrowdloanRewardAllocation<T> for Pallet<T> {
fn get_total_crowdloan_allocation() -> Option<BalanceOf<T>> {
IssuanceConfigStore::<T>::get()
.map(|issuance_config| issuance_config.total_crowdloan_allocation)
}
}
impl<T: Config> GetIssuance<BalanceOf<T>> for Pallet<T> {
fn get_all_issuance(n: u32) -> Option<(BalanceOf<T>, BalanceOf<T>)> {
SessionIssuance::<T>::get(n)
}
fn get_liquidity_mining_issuance(n: u32) -> Option<BalanceOf<T>> {
SessionIssuance::<T>::get(n).map(|(x, _)| x)
}
fn get_staking_issuance(n: u32) -> Option<BalanceOf<T>> {
SessionIssuance::<T>::get(n).map(|(_, x)| x)
}
}
impl<T: Config> Pallet<T> {
pub fn do_init_issuance_config() -> DispatchResultWithPostInfo {
ensure!(
IssuanceConfigStore::<T>::get().is_none(),
Error::<T>::IssuanceConfigAlreadyInitialized
);
ensure!(IsTGEFinalized::<T>::get(), Error::<T>::TGENotFinalized);
let issuance_config: IssuanceInfo<BalanceOf<T>> = IssuanceInfo {
cap: T::IssuanceCap::get(),
issuance_at_init: T::Tokens::total_issuance(T::NativeCurrencyId::get().into()),
linear_issuance_blocks: T::LinearIssuanceBlocks::get(),
liquidity_mining_split: T::LiquidityMiningSplit::get(),
staking_split: T::StakingSplit::get(),
total_crowdloan_allocation: T::TotalCrowdloanAllocation::get(),
};
Pallet::<T>::build_issuance_config(issuance_config.clone())?;
Pallet::<T>::deposit_event(Event::IssuanceConfigInitialized(issuance_config));
Ok(().into())
}
pub fn build_issuance_config(issuance_config: IssuanceInfo<BalanceOf<T>>) -> DispatchResult {
ensure!(
issuance_config
.liquidity_mining_split
.checked_add(&issuance_config.staking_split)
.ok_or(Error::<T>::IssuanceConfigInvalid)? ==
Perbill::from_percent(100),
Error::<T>::IssuanceConfigInvalid
);
ensure!(
issuance_config.cap >=
issuance_config
.issuance_at_init
.checked_add(&issuance_config.total_crowdloan_allocation)
.ok_or(Error::<T>::IssuanceConfigInvalid)?,
Error::<T>::IssuanceConfigInvalid
);
ensure!(
issuance_config.linear_issuance_blocks != u32::zero(),
Error::<T>::IssuanceConfigInvalid
);
ensure!(
issuance_config.linear_issuance_blocks > T::BlocksPerRound::get(),
Error::<T>::IssuanceConfigInvalid
);
ensure!(T::BlocksPerRound::get() != u32::zero(), Error::<T>::IssuanceConfigInvalid);
IssuanceConfigStore::<T>::put(issuance_config);
Ok(())
}
pub fn calculate_and_store_round_issuance(current_round: u32) -> DispatchResult {
let issuance_config =
IssuanceConfigStore::<T>::get().ok_or(Error::<T>::IssuanceConfigNotInitialized)?;
let to_be_issued: BalanceOf<T> = issuance_config
.cap
.checked_sub(&issuance_config.issuance_at_init)
.ok_or(Error::<T>::MathError)?
.checked_sub(&issuance_config.total_crowdloan_allocation)
.ok_or(Error::<T>::MathError)?;
let linear_issuance_sessions: u32 = issuance_config
.linear_issuance_blocks
.checked_div(T::BlocksPerRound::get())
.ok_or(Error::<T>::MathError)?;
let linear_issuance_per_session = to_be_issued
.checked_div(&linear_issuance_sessions.into())
.ok_or(Error::<T>::MathError)?;
let current_round_issuance: BalanceOf<T>;
if current_round < linear_issuance_sessions {
current_round_issuance = linear_issuance_per_session;
} else {
let current_mga_total_issuance: BalanceOf<T> =
T::Tokens::total_issuance(T::NativeCurrencyId::get().into());
if issuance_config.cap > current_mga_total_issuance {
current_round_issuance = linear_issuance_per_session.min(
issuance_config
.cap
.checked_sub(¤t_mga_total_issuance)
.ok_or(Error::<T>::MathError)?,
)
} else {
current_round_issuance = Zero::zero();
}
}
let liquidity_mining_issuance =
issuance_config.liquidity_mining_split * current_round_issuance;
let staking_issuance = issuance_config.staking_split * current_round_issuance;
T::LiquidityMiningApi::distribute_rewards(liquidity_mining_issuance);
{
let liquidity_mining_issuance_issued = T::Tokens::deposit_creating(
T::NativeCurrencyId::get().into(),
&T::LiquidityMiningIssuanceVault::get(),
liquidity_mining_issuance,
);
let staking_issuance_issued = T::Tokens::deposit_creating(
T::NativeCurrencyId::get().into(),
&T::StakingIssuanceVault::get(),
staking_issuance,
);
Self::deposit_event(Event::SessionIssuanceIssued(
current_round,
liquidity_mining_issuance_issued.peek(),
staking_issuance_issued.peek(),
));
}
SessionIssuance::<T>::insert(
current_round,
Some((liquidity_mining_issuance, staking_issuance)),
);
Pallet::<T>::deposit_event(Event::SessionIssuanceRecorded(
current_round,
liquidity_mining_issuance,
staking_issuance,
));
Ok(())
}
pub fn clear_round_issuance_history(current_round: u32) -> DispatchResult {
if current_round >= T::HistoryLimit::get() {
SessionIssuance::<T>::remove(current_round - T::HistoryLimit::get());
}
Ok(())
}
}