Newer
Older
// This file is part of Duniter-v2S.
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
mod compute_claim_uds;
mod types;
pub use types::*;
use frame_support::traits::{tokens::ExistenceRequirement, Currency, OnTimestampSet};
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 sp_runtime::traits::Convert;
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
//#[pallet::without_storage_info]
pub trait Config: frame_system::Config + pallet_timestamp::Config {
// Moment into Balance converter
type MomentIntoBalance: Convert<Self::Moment, BalanceOf<Self>>;
// The currency
type Currency: Currency<Self::AccountId>;
/// Maximum number of past UD revaluations to keep in storage.
#[pallet::constant]
type MaxPastReeval: Get<u32>;
/// Somethings that must provide the number of accounts allowed to create the universal dividend
type MembersCount: Get<BalanceOf<Self>>;
/// Somethings that must provide the list of accounts ids allowed to create the universal dividend
type MembersStorage: frame_support::traits::StoredMap<Self::AccountId, FirstEligibleUd>;
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Square of the money growth rate per ud reevaluation period
type SquareMoneyGrowthRate: Get<Perbill>;
type UdCreationPeriod: Get<Self::Moment>;
type UdReevalPeriod: Get<Self::Moment>;
/// The number of units to divide the amounts expressed in number of UDs
/// Example: If you wish to express the UD amounts with a maximum precision of the order
/// of the milliUD, choose 1000
#[pallet::constant]
/// Pallet weights info
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type AccountIdOf: Convert<u32, Option<Self::AccountId>>;
/// Current UD amount
#[pallet::getter(fn current_ud)]
pub type CurrentUd<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
// default value for number of the next UD
#[pallet::type_value]
pub fn DefaultForCurrentUdIndex() -> UdIndex {
1
}
/// Current UD index
// (more like the index of the ongoing UD = the next one)
#[pallet::storage]
#[pallet::getter(fn ud_index)]
pub type CurrentUdIndex<T: Config> =
StorageValue<_, UdIndex, ValueQuery, DefaultForCurrentUdIndex>;
#[cfg(test)]
#[pallet::storage]
// UD should be linked to idtyid instead of accountid
// if it is convenient in test, why not have it in runtime also?
// storing it in idty_value.data is strange
pub type TestMembers<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
FirstEligibleUd,
ValueQuery,
GetDefault,
ConstU32<300_000>,
>;
/// Total quantity of money created by universal dividend (does not take into account the possible destruction of money)
#[pallet::storage]
#[pallet::getter(fn total_money_created)]
pub type MonetaryMass<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
/// Next UD reevaluation
#[pallet::storage]
#[pallet::getter(fn next_reeval)]
pub type NextReeval<T: Config> = StorageValue<_, T::Moment, OptionQuery>;
/// Next UD creation
#[pallet::storage]
#[pallet::getter(fn next_ud)]
pub type NextUd<T: Config> = StorageValue<_, T::Moment, OptionQuery>;
/// Past UD reevaluations
#[pallet::storage]
#[pallet::getter(fn past_reevals)]
pub type PastReevals<T: Config> =
StorageValue<_, BoundedVec<(UdIndex, BalanceOf<T>), T::MaxPastReeval>, ValueQuery>;
pub struct GenesisConfig<T: Config>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
/// moment of the first UD reeval
// If None, it will be set to one period after the first block with a timestamp
/// moment of the first UD generation
// If None, it will be set to one period after the first block with a timestamp
/// initial monetary mass (should match total issuance)
/// accounts of initial members
// (only for test purpose)
#[cfg(test)]
pub initial_members: Vec<T::AccountId>,
/// value of the first UD
/// expressed in amount of currency
impl<T: Config> Default for GenesisConfig<T>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
#[cfg(test)]
initial_members: Default::default(),
impl<T: Config> BuildGenesisConfig for GenesisConfig<T>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
// totalissuance should be updated to the same amount
<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
.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());
}
}
}
}
// EVENTS //
// Pallets use events to inform users when important changes are made.
// https://substrate.dev/docs/en/knowledgebase/runtime/events
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A new universal dividend is created.
NewUdCreated {
amount: BalanceOf<T>,
index: UdIndex,
monetary_mass: BalanceOf<T>,
members_count: BalanceOf<T>,
},
/// The universal dividend has been re-evaluated.
UdReevalued {
new_ud_amount: BalanceOf<T>,
monetary_mass: BalanceOf<T>,
members_count: BalanceOf<T>,
},
/// DUs were automatically transferred as part of a member removal.
count: UdIndex,
total: BalanceOf<T>,
who: T::AccountId,
},
/// A member claimed his UDs.
UdsClaimed {
count: UdIndex,
total: BalanceOf<T>,
who: T::AccountId,
},
}
// ERRORS //
#[pallet::error]
pub enum Error<T> {
/// This account is not allowed to claim UDs.
AccountNotAllowedToClaimUds,
}
// INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> {
/// create universal dividend
pub(crate) fn create_ud(members_count: BalanceOf<T>) {
// get current value of UD and monetary mass
let ud_amount = <CurrentUd<T>>::get();
let monetary_mass = <MonetaryMass<T>>::get();
// Increment ud index
let ud_index = CurrentUdIndex::<T>::mutate(|next_ud_index| {
core::mem::replace(next_ud_index, next_ud_index.saturating_add(1))
});
// compute the new monetary mass
let new_monetary_mass =
monetary_mass.saturating_add(ud_amount.saturating_mul(members_count));
// update the storage value of the monetary mass
MonetaryMass::<T>::put(new_monetary_mass);
// emit an event to inform blockchain users that the holy UNIVERSAL DIVIDEND was created
Self::deposit_event(Event::NewUdCreated {
amount: ud_amount,
index: ud_index,
members_count,
monetary_mass: new_monetary_mass,
});
}
/// claim all due universal dividend at a time
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."),
);
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())
}
})
/// like balance.transfer, but give an amount in UD
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())
}
/// reevaluate the value of the universal dividend
pub(crate) fn reeval_ud(members_count: BalanceOf<T>) {
// get current value and monetary mass
let ud_amount = <CurrentUd<T>>::get();
let monetary_mass = <MonetaryMass<T>>::get();
// compute new value
let new_ud_amount = Self::reeval_ud_formula(
ud_amount,
T::SquareMoneyGrowthRate::get(),
monetary_mass,
members_count,
T::UdReevalPeriod::get() / T::UdCreationPeriod::get(),
),
// update the storage value and the history of past reevals
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 {
/// formula for Universal Dividend reevaluation
c_square: Perbill,
mut members_count: BalanceOf<T>,
count_uds_beetween_two_reevals: BalanceOf<T>, // =(dt/udFrequency)
) -> BalanceOf<T> {
if members_count.is_zero() {
members_count = One::one();
}
// UD(t+1) = UD(t) + c² (M(t+1) / N(t+1)) / (dt/udFrequency)
ud_t + (c_square * monetary_mass) / (members_count * count_uds_beetween_two_reevals)
// CALLS //
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Claim Universal Dividends
#[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)
}
/// Transfer some liquid free balance to another account, in milliUD.
#[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)
}
/// Transfer some liquid free balance to another account, in milliUD.
#[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)
}
}
// PUBLIC FUNCTIONS
impl<T: Config> Pallet<T> {
pub fn init_first_eligible_ud() -> FirstEligibleUd {
CurrentUdIndex::<T>::get().into()
}
/// function to call when removing a member
/// auto-claims UDs
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(),
);
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)
}
}
}
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
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
});
// Reevaluation may happen later than expected, but this has no effect before a new UD
// is created. This is why we can check for reevaluation only when creating UD.
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()));
}
}
}