#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
mod compute_claim_uds;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod types;
mod weights;
pub use pallet::*;
pub use types::*;
pub use weights::WeightInfo;
use frame_support::traits::{tokens::ExistenceRequirement, Currency, OnTimestampSet};
use sp_arithmetic::{
per_things::Perbill,
traits::{One, Saturating, Zero},
};
use sp_runtime::traits::{Get, MaybeSerializeDeserialize, StaticLookup};
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_support::traits::{StorageVersion, StoredMap};
use frame_system::pallet_prelude::*;
use sp_runtime::traits::Convert;
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + pallet_timestamp::Config {
type MomentIntoBalance: Convert<Self::Moment, BalanceOf<Self>>;
type Currency: Currency<Self::AccountId>;
#[pallet::constant]
type MaxPastReeval: Get<u32>;
type MembersCount: Get<BalanceOf<Self>>;
type MembersStorage: frame_support::traits::StoredMap<Self::AccountId, FirstEligibleUd>;
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
#[pallet::constant]
type SquareMoneyGrowthRate: Get<Perbill>;
#[pallet::constant]
type UdCreationPeriod: Get<Self::Moment>;
#[pallet::constant]
type UdReevalPeriod: Get<Self::Moment>;
#[pallet::constant]
type UnitsPerUd: Get<BalanceOf<Self>>;
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type AccountIdOf: Convert<u32, Option<Self::AccountId>>;
}
#[pallet::storage]
#[pallet::getter(fn current_ud)]
pub type CurrentUd<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::type_value]
pub fn DefaultForCurrentUdIndex() -> UdIndex {
1
}
#[pallet::storage]
#[pallet::getter(fn ud_index)]
pub type CurrentUdIndex<T: Config> =
StorageValue<_, UdIndex, ValueQuery, DefaultForCurrentUdIndex>;
#[cfg(test)]
#[pallet::storage]
pub type TestMembers<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
FirstEligibleUd,
ValueQuery,
GetDefault,
ConstU32<300_000>,
>;
#[pallet::storage]
#[pallet::getter(fn total_money_created)]
pub type MonetaryMass<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn next_reeval)]
pub type NextReeval<T: Config> = StorageValue<_, T::Moment, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn next_ud)]
pub type NextUd<T: Config> = StorageValue<_, T::Moment, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn past_reevals)]
pub type PastReevals<T: Config> =
StorageValue<_, BoundedVec<(UdIndex, BalanceOf<T>), T::MaxPastReeval>, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
pub first_reeval: Option<T::Moment>,
pub first_ud: Option<T::Moment>,
pub initial_monetary_mass: BalanceOf<T>,
#[cfg(test)]
pub initial_members: Vec<T::AccountId>,
pub ud: BalanceOf<T>,
}
impl<T: Config> Default for GenesisConfig<T>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
fn default() -> Self {
Self {
first_reeval: None,
first_ud: None,
initial_monetary_mass: Default::default(),
#[cfg(test)]
initial_members: Default::default(),
ud: BalanceOf::<T>::one(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
fn build(&self) {
assert!(!self.ud.is_zero());
<CurrentUd<T>>::put(self.ud);
<MonetaryMass<T>>::put(self.initial_monetary_mass);
NextReeval::<T>::set(self.first_reeval);
NextUd::<T>::set(self.first_ud);
let mut past_reevals = BoundedVec::default();
past_reevals
.try_push((1, self.ud))
.expect("MaxPastReeval should be greather than zero");
PastReevals::<T>::put(past_reevals);
#[cfg(test)]
{
for member in &self.initial_members {
TestMembers::<T>::insert(member, FirstEligibleUd::min());
}
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
NewUdCreated {
amount: BalanceOf<T>,
index: UdIndex,
monetary_mass: BalanceOf<T>,
members_count: BalanceOf<T>,
},
UdReevalued {
new_ud_amount: BalanceOf<T>,
monetary_mass: BalanceOf<T>,
members_count: BalanceOf<T>,
},
UdsAutoPaid {
count: UdIndex,
total: BalanceOf<T>,
who: T::AccountId,
},
UdsClaimed {
count: UdIndex,
total: BalanceOf<T>,
who: T::AccountId,
},
}
#[pallet::error]
pub enum Error<T> {
AccountNotAllowedToClaimUds,
}
impl<T: Config> Pallet<T> {
pub(crate) fn create_ud(members_count: BalanceOf<T>) {
let ud_amount = <CurrentUd<T>>::get();
let monetary_mass = <MonetaryMass<T>>::get();
let ud_index = CurrentUdIndex::<T>::mutate(|next_ud_index| {
core::mem::replace(next_ud_index, next_ud_index.saturating_add(1))
});
let new_monetary_mass =
monetary_mass.saturating_add(ud_amount.saturating_mul(members_count));
MonetaryMass::<T>::put(new_monetary_mass);
Self::deposit_event(Event::NewUdCreated {
amount: ud_amount,
index: ud_index,
members_count,
monetary_mass: new_monetary_mass,
});
}
fn do_claim_uds(who: &T::AccountId) -> DispatchResultWithPostInfo {
T::MembersStorage::try_mutate_exists(who, |maybe_first_eligible_ud| {
if let Some(FirstEligibleUd(Some(ref mut first_ud_index))) = maybe_first_eligible_ud
{
let current_ud_index = CurrentUdIndex::<T>::get();
if first_ud_index.get() >= current_ud_index {
DispatchResultWithPostInfo::Ok(().into())
} else {
let (uds_count, uds_total) = compute_claim_uds::compute_claim_uds(
current_ud_index,
first_ud_index.get(),
PastReevals::<T>::get().into_iter(),
);
let _ = core::mem::replace(
first_ud_index,
core::num::NonZeroU16::new(current_ud_index)
.expect("unreachable because current_ud_index is never zero."),
);
let _ = T::Currency::deposit_creating(who, uds_total);
Self::deposit_event(Event::UdsClaimed {
count: uds_count,
total: uds_total,
who: who.clone(),
});
Ok(().into())
}
} else {
Err(Error::<T>::AccountNotAllowedToClaimUds.into())
}
})
}
fn do_transfer_ud(
origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source,
value: BalanceOf<T>,
existence_requirement: ExistenceRequirement,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let dest = T::Lookup::lookup(dest)?;
let ud_amount = <CurrentUd<T>>::get();
T::Currency::transfer(
&who,
&dest,
value.saturating_mul(ud_amount) / T::UnitsPerUd::get(),
existence_requirement,
)?;
Ok(().into())
}
pub(crate) fn reeval_ud(members_count: BalanceOf<T>) {
let ud_amount = <CurrentUd<T>>::get();
let monetary_mass = <MonetaryMass<T>>::get();
let new_ud_amount = Self::reeval_ud_formula(
ud_amount,
T::SquareMoneyGrowthRate::get(),
monetary_mass,
members_count,
T::MomentIntoBalance::convert(
T::UdReevalPeriod::get() / T::UdCreationPeriod::get(),
),
);
CurrentUd::<T>::put(new_ud_amount);
PastReevals::<T>::mutate(|past_reevals| {
if past_reevals.len() == T::MaxPastReeval::get() as usize {
past_reevals.remove(0);
}
past_reevals
.try_push((CurrentUdIndex::<T>::get(), new_ud_amount))
.expect("Unreachable, because we removed an element just before.")
});
Self::deposit_event(Event::UdReevalued {
new_ud_amount,
monetary_mass,
members_count,
});
}
fn reeval_ud_formula(
ud_t: BalanceOf<T>,
c_square: Perbill,
monetary_mass: BalanceOf<T>,
mut members_count: BalanceOf<T>,
count_uds_beetween_two_reevals: BalanceOf<T>, ) -> BalanceOf<T> {
if members_count.is_zero() {
members_count = One::one();
}
ud_t + (c_square * monetary_mass) / (members_count * count_uds_beetween_two_reevals)
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::claim_uds(T::MaxPastReeval::get()))]
pub fn claim_uds(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Self::do_claim_uds(&who)
}
#[pallet::call_index(1)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::transfer_ud())]
pub fn transfer_ud(
origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] value: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
Self::do_transfer_ud(origin, dest, value, ExistenceRequirement::AllowDeath)
}
#[pallet::call_index(2)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::transfer_ud_keep_alive())]
pub fn transfer_ud_keep_alive(
origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] value: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
Self::do_transfer_ud(origin, dest, value, ExistenceRequirement::KeepAlive)
}
}
impl<T: Config> Pallet<T> {
pub fn init_first_eligible_ud() -> FirstEligibleUd {
CurrentUdIndex::<T>::get().into()
}
pub fn on_removed_member(first_ud_index: UdIndex, who: &T::AccountId) -> Weight {
let current_ud_index = CurrentUdIndex::<T>::get();
if first_ud_index < current_ud_index {
let (uds_count, uds_total) = compute_claim_uds::compute_claim_uds(
current_ud_index,
first_ud_index,
PastReevals::<T>::get().into_iter(),
);
let _ = T::Currency::deposit_creating(who, uds_total);
Self::deposit_event(Event::UdsAutoPaid {
count: uds_count,
total: uds_total,
who: who.clone(),
});
<T as pallet::Config>::WeightInfo::on_removed_member(first_ud_index as u32)
} else {
<T as pallet::Config>::WeightInfo::on_removed_member(0)
}
}
}
}
impl<T: Config> OnTimestampSet<T::Moment> for Pallet<T>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
fn on_timestamp_set(moment: T::Moment) {
let next_ud = NextUd::<T>::get().unwrap_or_else(|| {
let next_ud = moment.saturating_add(T::UdCreationPeriod::get());
NextUd::<T>::put(next_ud);
next_ud
});
if moment >= next_ud {
let current_members_count = T::MembersCount::get();
let next_reeval = NextReeval::<T>::get().unwrap_or_else(|| {
let next_reeval = moment.saturating_add(T::UdReevalPeriod::get());
NextReeval::<T>::put(next_reeval);
next_reeval
});
if moment >= next_reeval {
NextReeval::<T>::put(next_reeval.saturating_add(T::UdReevalPeriod::get()));
Self::reeval_ud(current_members_count);
}
Self::create_ud(current_members_count);
NextUd::<T>::put(next_ud.saturating_add(T::UdCreationPeriod::get()));
}
}
}