Skip to content
Snippets Groups Projects
lib.rs 18 KiB
Newer Older
Éloïs's avatar
Éloïs committed
// Copyright 2021 Axiom-Team
//
// This file is part of Duniter-v2S.
Éloïs's avatar
Éloïs committed
//
// Duniter-v2S is free software: you can redistribute it and/or modify
Éloïs's avatar
Éloïs committed
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
Éloïs's avatar
Éloïs committed
//
// Duniter-v2S is distributed in the hope that it will be useful,
Éloïs's avatar
Éloïs committed
// 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/>.
Éloïs's avatar
Éloïs committed

#![cfg_attr(not(feature = "std"), no_std)]

mod benchmarking;
Éloïs's avatar
Éloïs committed
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod weights;
Éloïs's avatar
Éloïs committed

pub use pallet::*;
pub use weights::WeightInfo;
Éloïs's avatar
Éloïs committed

use frame_support::traits::{tokens::ExistenceRequirement, Currency, OnTimestampSet};
use sp_arithmetic::{
    traits::{One, Saturating, Zero},
use sp_runtime::traits::{Get, MaybeSerializeDeserialize, StaticLookup};
Éloïs's avatar
Éloïs committed
#[frame_support::pallet]
pub mod pallet {
    use super::*;
    use frame_support::pallet_prelude::*;
    use frame_support::traits::{StorageVersion, StoredMap};
Éloïs's avatar
Éloïs committed
    use frame_system::pallet_prelude::*;
    use sp_runtime::traits::Convert;
Éloïs's avatar
Éloïs committed

    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)]
    pub struct Pallet<T>(_);

Éloïs's avatar
Éloïs committed
    #[pallet::config]
    pub trait Config: frame_system::Config + pallet_timestamp::Config {
        // Moment into Balance converter
        type MomentIntoBalance: Convert<Self::Moment, BalanceOf<Self>>;
Éloïs's avatar
Éloïs committed
        // The currency
        type Currency: Currency<Self::AccountId>;
        /// Maximum number of past UD revaluations to keep in storage.
Éloïs's avatar
Éloïs committed
        /// 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>;
Éloïs's avatar
Éloïs committed
        /// Square of the money growth rate per ud reevaluation period
        #[pallet::constant]
        type SquareMoneyGrowthRate: Get<Perbill>;
        /// Universal dividend creation period (ms)
        #[pallet::constant]
        type UdCreationPeriod: Get<Self::Moment>;
        /// Universal dividend reevaluation period (ms)
        #[pallet::constant]
        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
        type UnitsPerUd: Get<BalanceOf<Self>>;
        /// Pallet weights info
        type WeightInfo: WeightInfo;
        #[cfg(feature = "runtime-benchmarks")]
        type AccountIdOf: Convert<u32, Option<Self::AccountId>>;
Éloïs's avatar
Éloïs committed
    }

    // STORAGE //

Éloïs's avatar
Éloïs committed
    #[pallet::storage]
    #[pallet::getter(fn current_ud)]
    pub type CurrentUd<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
Éloïs's avatar
Éloïs committed

    // 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>,
    >;

Éloïs's avatar
Éloïs committed
    /// 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>;
Éloïs's avatar
Éloïs committed

    /// Past UD reevaluations
    #[pallet::storage]
    #[pallet::getter(fn past_reevals)]
    pub type PastReevals<T: Config> =
        StorageValue<_, BoundedVec<(UdIndex, BalanceOf<T>), T::MaxPastReeval>, ValueQuery>;

Éloïs's avatar
Éloïs committed
    // GENESIS

    #[pallet::genesis_config]
    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
        pub first_reeval: Option<T::Moment>,
        /// moment of the first UD generation
        // If None, it will be set to one period after the first block with a timestamp
        pub first_ud: Option<T::Moment>,
        /// initial monetary mass (should match total issuance)
Éloïs's avatar
Éloïs committed
        pub initial_monetary_mass: BalanceOf<T>,
        /// 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
        pub ud: BalanceOf<T>,
    impl<T: Config> Default for GenesisConfig<T>
    where
        <T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
    {
Éloïs's avatar
Éloïs committed
        fn default() -> Self {
            Self {
                first_reeval: None,
                first_ud: None,
Éloïs's avatar
Éloïs committed
                initial_monetary_mass: Default::default(),
                #[cfg(test)]
                initial_members: Default::default(),
                ud: Default::default(),
Éloïs's avatar
Éloïs committed
            }
        }
    }

    #[pallet::genesis_build]
    impl<T: Config> BuildGenesisConfig for GenesisConfig<T>
    where
        <T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
    {
Éloïs's avatar
Éloïs committed
        fn build(&self) {
            assert!(!self.ud.is_zero());
Éloïs's avatar
Éloïs committed

            <CurrentUd<T>>::put(self.ud);
            // 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
                .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());
                }
            }
Éloïs's avatar
Éloïs committed
        }
    }

    // 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>,
            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,
Éloïs's avatar
Éloïs committed
    }

    // 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();
Éloïs's avatar
Éloïs committed

            // Increment ud index
            let ud_index = CurrentUdIndex::<T>::mutate(|next_ud_index| {
                core::mem::replace(next_ud_index, next_ud_index.saturating_add(1))
            });
Éloïs's avatar
Éloïs committed

            // 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,
                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())
                }
            })
Éloïs's avatar
Éloïs committed
        }

        /// 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)?;
            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();
Éloïs's avatar
Éloïs committed

Éloïs's avatar
Éloïs committed
            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(),
                ),
Éloïs's avatar
Éloïs committed
            );

            // 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 {
Éloïs's avatar
Éloïs committed
                new_ud_amount,
                monetary_mass,
                members_count,
Éloïs's avatar
Éloïs committed
        }

        /// formula for Universal Dividend reevaluation
Éloïs's avatar
Éloïs committed
        fn reeval_ud_formula(
            ud_t: BalanceOf<T>,
Éloïs's avatar
Éloïs committed
            monetary_mass: BalanceOf<T>,
            mut members_count: BalanceOf<T>,
Éloïs's avatar
Éloïs committed
            count_uds_beetween_two_reevals: BalanceOf<T>, // =(dt/udFrequency)
        ) -> BalanceOf<T> {
Pascal Engélibert's avatar
Pascal Engélibert committed
            // Ensure that we do not divide by zero
            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)
Éloïs's avatar
Éloïs committed
        }
    }

    // CALLS //

    #[pallet::call]
    impl<T: Config> Pallet<T> {
        #[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::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::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)
                <T as pallet::Config>::WeightInfo::on_removed_member(0)
Éloïs's avatar
Éloïs committed
}

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()));
        }
    }
}