Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1000i100-test
  • 105_gitlab_container_registry
  • cgeek/issue-297-cpu
  • ci_cache
  • debug/podman
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-smoldot
  • feature/dc-dump
  • feature/distance-rule
  • feature/show_milestone
  • fix-252
  • fix_picked_up_file_in_runtime_release
  • gdev-800-tests
  • hugo-release/runtime-701
  • hugo-tmp-dockerfile-cache
  • hugo/195-doc
  • hugo/195-graphql-schema
  • hugo/distance-precompute
  • hugo/endpoint-gossip
  • hugo/tmp-0.9.1
  • master
  • network/gdev-800
  • network/gdev-802
  • network/gdev-803
  • network/gdev-900
  • network/gtest-1000
  • pini-check-password
  • release/client-800.2
  • release/hugo-chainspec-gdev5
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • release/runtime-401
  • release/runtime-500
  • release/runtime-600
  • release/runtime-700
  • release/runtime-701
  • release/runtime-800
  • runtime/gtest-1000
  • tests/distance-with-oracle
  • tuxmain/anonymous-tx
  • tuxmain/benchmark-distance
  • tuxmain/fix-change-owner-key
  • update-docker-compose-rpc-squid-names
  • upgradable-multisig
  • gdev-800
  • gdev-800-0.8.0
  • gdev-802
  • gdev-803
  • gdev-900-0.10.0
  • gdev-900-0.10.1
  • gdev-900-0.9.0
  • gdev-900-0.9.1
  • gdev-900-0.9.2
  • gtest-1000
  • gtest-1000-0.11.0
  • gtest-1000-0.11.1
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • runtime-401
  • runtime-500
  • runtime-600
  • runtime-700
  • runtime-701
  • runtime-800
  • runtime-800-backup
  • runtime-800-bis
  • runtime-801
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
  • v0.4.1
88 results

Target

Select target project
  • nodes/rust/duniter-v2s
  • llaq/lc-core-substrate
  • pini-gh/duniter-v2s
  • vincentux/duniter-v2s
  • mildred/duniter-v2s
  • d0p1/duniter-v2s
  • bgallois/duniter-v2s
  • Nicolas80/duniter-v2s
8 results
Select Git revision
  • archive_upgrade_polkadot_v0.9.42
  • david-wot-scenarios-cucumber
  • distance
  • elois-ci-binary-release
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-fix-85
  • elois-fix-idty-post-genesis
  • elois-fix-sufficients-change-owner-key
  • elois-opti-cert
  • elois-remove-renewable-period
  • elois-revoc-with-old-key
  • elois-rework-certs
  • elois-smish-members-cant-change-or-rem-idty
  • elois-smoldot
  • elois-substrate-v0.9.23
  • elois-technical-commitee
  • hugo-gtest
  • hugo-remove-duniter-account
  • hugo-rework-genesis
  • hugo-tmp
  • jrx/workspace_tomls
  • master
  • no-bootnodes
  • pallet-benchmark
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • test-gen-new-owner-key-msg
  • ts-types
  • ud-time-64
  • upgrade_polkadot_v0.9.42
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
52 results
Show changes
Showing
with 2682 additions and 476 deletions
use crate::{Config, CurrentSession, Pallet};
use pallet_authority_members::SessionIndex;
use sp_runtime::traits::Convert;
impl<T: Config> pallet_authority_members::OnOutgoingMember<T::MemberId> for Pallet<T> {
fn on_outgoing_member(member_id: T::MemberId) {
if let Some(member_id) = T::IdtyIdOfAuthorityId::convert(member_id) {
Pallet::<T>::on_smith_goes_offline(member_id);
}
}
}
/// As long as a Smith is in the authority set, he will not expire.
impl<T: Config> pallet_authority_members::OnIncomingMember<T::MemberId> for Pallet<T> {
fn on_incoming_member(member_id: T::MemberId) {
if let Some(member_id) = T::IdtyIdOfAuthorityId::convert(member_id) {
Pallet::<T>::on_smith_goes_online(member_id);
}
}
}
impl<T: Config> pallet_authority_members::OnNewSession for Pallet<T> {
fn on_new_session(index: SessionIndex) {
CurrentSession::<T>::put(index);
Pallet::<T>::on_exclude_expired_smiths(index);
}
}
// Copyright 2021-2023 Axiom-Team
//
// 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/>.
//! # Duniter Smith Pallet
//!
//! The Smith pallet in Duniter serves as a bridge between the `identity` and `authority-members` pallets.
//!
//! ## Overview
//!
//! The Smith pallet manages the certification and membership status of Smiths. Smiths are identities that have met certain requirements and play a critical role in the network's operations (block authoring, distance evaluation).
//!
//! ## Key Concepts
//!
//! ### Smith Status
//!
//! The status of an identity within the Smith pallet can be one of the following:
//! - **Invited**: The identity has been invited by a Smith but has not yet accepted the invitation.
//! - **Pending**: The identity has accepted the invitation and is pending to become a full Smith.
//! - **Smith**: The identity has fulfilled the requirements and is a full-fledged Smith, eligible to perform critical network functions.
//! - **Excluded**: The identity has been removed from the Smiths set but its certifications are retained for tracking purposes.
//!
//! ### Certifications
//!
//! Certifications are crucial in determining Smith status:
//! - An identity needs a minimum number of certifications to become a Smith (`MinCertForMembership`).
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod impls;
pub mod traits;
mod types;
pub mod weights;
mod benchmarking;
use codec::{Codec, Decode, Encode};
use duniter_primitives::Idty;
use frame_support::{
dispatch::DispatchResultWithPostInfo,
ensure,
pallet_prelude::{Get, RuntimeDebug, Weight},
};
use frame_system::{
ensure_signed,
pallet_prelude::{BlockNumberFor, OriginFor},
};
use scale_info::{
prelude::{collections::BTreeMap, fmt::Debug, vec, vec::Vec},
TypeInfo,
};
use sp_runtime::traits::{AtLeast32BitUnsigned, IsMember};
use crate::traits::OnSmithDelete;
pub use crate::weights::WeightInfo;
pub use pallet::*;
use pallet_authority_members::SessionIndex;
pub use types::*;
/// Reasons for the removal of a Smith identity.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithRemovalReason {
/// Membership was lost due to expiration or other reasons.
LostMembership,
/// Smith was offline for too long.
OfflineTooLong,
/// Smith was blacklisted.
Blacklisted,
}
/// Possible statuses of a Smith identity.
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub enum SmithStatus {
/// The identity has been invited by a Smith but has not accepted yet.
Invited,
/// The identity has accepted to eventually become a Smith.
Pending,
/// The identity has reached the requirements to become a Smith and can now perform Smith operations.
Smith,
/// The identity has been removed from the Smiths set but is kept to track its certifications.
Excluded,
}
#[allow(unreachable_patterns)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{pallet_prelude::*, traits::StorageVersion};
use pallet_authority_members::SessionIndex;
use sp_runtime::traits::{Convert, IsMember};
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
/// The pallet's config trait.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Trait to check if identity is a WoT members.
type IsWoTMember: IsMember<Self::IdtyIndex>;
type OnSmithDelete: traits::OnSmithDelete<Self::IdtyIndex>;
/// The overarching event type for this pallet.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// A short identity index type.
type IdtyIndex: Parameter
+ Member
+ AtLeast32BitUnsigned
+ Codec
+ Default
+ Copy
+ MaybeSerializeDeserialize
+ Debug
+ MaxEncodedLen;
/// Identifier type for an authority-member.
type MemberId: Copy + Ord + MaybeSerializeDeserialize + Parameter;
/// Something that gives the IdtyIndex of an AccountId and reverse.
type IdtyAttr: duniter_primitives::Idty<Self::IdtyIndex, Self::AccountId>;
/// Something that gives the AccountId of an identity.
type IdtyIdOfAuthorityId: Convert<Self::MemberId, Option<Self::IdtyIndex>>;
/// Maximum number of active certifications per issuer.
#[pallet::constant]
type MaxByIssuer: Get<u32>;
/// Minimum number of certifications required to become a Smith.
#[pallet::constant]
type MinCertForMembership: Get<u32>;
/// Maximum duration of inactivity allowed before a Smith is removed.
#[pallet::constant]
type SmithInactivityMaxDuration: Get<u32>;
/// Type representing the weight of this pallet.
type WeightInfo: WeightInfo;
}
/// Events type.
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// An identity is being inivited to become a smith.
InvitationSent {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
/// The invitation has been accepted.
InvitationAccepted { idty_index: T::IdtyIndex },
/// Certification received
SmithCertAdded {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
/// Certification lost
SmithCertRemoved {
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
},
/// A smith gathered enough certifications to become an authority (can call `go_online()`).
SmithMembershipAdded { idty_index: T::IdtyIndex },
/// A smith has been removed from the smiths set.
SmithMembershipRemoved { idty_index: T::IdtyIndex },
}
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub initial_smiths: BTreeMap<T::IdtyIndex, (bool, Vec<T::IdtyIndex>)>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
initial_smiths: Default::default(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
CurrentSession::<T>::put(0);
let mut cert_meta_by_issuer = BTreeMap::<T::IdtyIndex, Vec<T::IdtyIndex>>::new();
for (receiver, (is_online, issuers)) in &self.initial_smiths {
// Forbid self-cert
assert!(
!issuers.contains(receiver),
"Identity cannot certify it-self."
);
let mut issuers_: Vec<_> = Vec::with_capacity(issuers.len());
for issuer in issuers {
// Count issued certs
cert_meta_by_issuer
.entry(*issuer)
.or_insert(vec![])
.push(*receiver);
issuers_.push(*issuer);
}
// Write CertsByReceiver
issuers_.sort();
let issuers_count = issuers_.len();
let smith_status = if issuers_count >= T::MinCertForMembership::get() as usize {
SmithStatus::Smith
} else {
SmithStatus::Pending
};
Smiths::<T>::insert(
receiver,
SmithMeta {
status: smith_status,
expires_on: if *is_online {
None
} else {
Some(CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get())
},
issued_certs: vec![],
received_certs: issuers_,
last_online: None,
},
);
// if smith is offline, schedule expire
if !*is_online {
ExpiresOn::<T>::append(
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get(),
receiver,
);
}
}
for (issuer, issued_certs) in cert_meta_by_issuer {
// Write CertsByIssuer
Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.issued_certs = issued_certs;
}
});
}
}
}
/// The Smith metadata for each identity.
#[pallet::storage]
#[pallet::getter(fn smiths)]
pub type Smiths<T: Config> = StorageMap<
_,
Twox64Concat,
T::IdtyIndex,
SmithMeta<T::IdtyIndex, BlockNumberFor<T>>,
OptionQuery,
>;
/// The indexes of Smith to remove at a given session.
#[pallet::storage]
#[pallet::getter(fn expires_on)]
pub type ExpiresOn<T: Config> =
StorageMap<_, Twox64Concat, SessionIndex, Vec<T::IdtyIndex>, OptionQuery>;
/// The current session index.
#[pallet::storage]
#[pallet::getter(fn current_session)]
pub type CurrentSession<T: Config> = StorageValue<_, SessionIndex, ValueQuery>;
// ERRORS //
#[pallet::error]
pub enum Error<T> {
/// Issuer of anything (invitation, acceptance, certification) must have an identity ID
OriginMustHaveAnIdentity,
/// Issuer must be known as a potential smith
OriginHasNeverBeenInvited,
/// Invitation is reseverd to smiths
InvitationIsASmithPrivilege,
/// Invitation is reseverd to online smiths
InvitationIsAOnlineSmithPrivilege,
/// Invitation must not have been accepted yet
InvitationAlreadyAccepted,
/// Invitation of an already known smith is forbidden except if it has been excluded
InvitationOfExistingNonExcluded,
/// Invitation of a non-member (of the WoT) is forbidden
InvitationOfNonMember,
/// Certification cannot be made on someone who has not accepted an invitation
CertificationMustBeAgreed,
/// Certification cannot be made on excluded
CertificationOnExcludedIsForbidden,
/// Issuer must be a smith
CertificationIsASmithPrivilege,
/// Only online smiths can certify
CertificationIsAOnlineSmithPrivilege,
/// Smith cannot certify itself
CertificationOfSelfIsForbidden,
/// Receiver must be invited by another smith
CertificationReceiverMustHaveBeenInvited,
/// Receiver must not already have this certification
CertificationAlreadyExists,
/// A smith has a limited stock of certifications
CertificationStockFullyConsumed,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Invite a member of the Web of Trust to attempt becoming a Smith.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::invite_smith())]
pub fn invite_smith(
origin: OriginFor<T>,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let issuer =
T::IdtyAttr::idty_index(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_invite_smith(issuer, receiver)?;
Self::do_invite_smith(issuer, receiver);
Ok(().into())
}
/// Accept an invitation to become a Smith (must have been invited first).
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::accept_invitation())]
pub fn accept_invitation(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin.clone())?;
let receiver =
T::IdtyAttr::idty_index(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_accept_invitation(receiver)?;
Self::do_accept_invitation(receiver)?;
Ok(().into())
}
/// Certify an invited Smith, which can lead the certified to become a Smith.
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::certify_smith())]
pub fn certify_smith(
origin: OriginFor<T>,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let issuer =
T::IdtyAttr::idty_index(who.clone()).ok_or(Error::<T>::OriginMustHaveAnIdentity)?;
Self::check_certify_smith(issuer, receiver)?;
Self::do_certify_smith(receiver, issuer);
Ok(().into())
}
}
}
impl<T: Config> Pallet<T> {
/// Check conditions before inviting a potential Smith.
fn check_invite_smith(
issuer: T::IdtyIndex,
receiver: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
let issuer = Smiths::<T>::get(issuer).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
ensure!(
issuer.status == SmithStatus::Smith,
Error::<T>::InvitationIsASmithPrivilege
);
ensure!(
issuer.expires_on.is_none(),
Error::<T>::InvitationIsAOnlineSmithPrivilege
);
if let Some(receiver_meta) = Smiths::<T>::get(receiver) {
ensure!(
receiver_meta.status == SmithStatus::Excluded,
Error::<T>::InvitationOfExistingNonExcluded
);
}
ensure!(
T::IsWoTMember::is_member(&receiver),
Error::<T>::InvitationOfNonMember
);
Ok(().into())
}
/// Perform the invitation of a potential Smith.
fn do_invite_smith(issuer: T::IdtyIndex, receiver: T::IdtyIndex) {
let new_expires_on = CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
let mut existing = Smiths::<T>::get(receiver).unwrap_or_default();
existing.status = SmithStatus::Invited;
existing.expires_on = Some(new_expires_on);
existing.received_certs = vec![];
Smiths::<T>::insert(receiver, existing);
ExpiresOn::<T>::append(new_expires_on, receiver);
Self::deposit_event(Event::<T>::InvitationSent { issuer, receiver });
}
/// Check conditions before accepting an invitation to become a Smith.
fn check_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
let pretender_status = Smiths::<T>::get(receiver)
.ok_or(Error::<T>::OriginHasNeverBeenInvited)?
.status;
ensure!(
pretender_status == SmithStatus::Invited,
Error::<T>::InvitationAlreadyAccepted
);
Ok(().into())
}
/// Accept the invitation to become a Smith.
fn do_accept_invitation(receiver: T::IdtyIndex) -> DispatchResultWithPostInfo {
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.status = SmithStatus::Pending;
}
});
Self::deposit_event(Event::<T>::InvitationAccepted {
idty_index: receiver,
});
Ok(().into())
}
/// Check conditions before certifying a potential Smith.
fn check_certify_smith(
issuer_index: T::IdtyIndex,
receiver_index: T::IdtyIndex,
) -> DispatchResultWithPostInfo {
ensure!(
issuer_index != receiver_index,
Error::<T>::CertificationOfSelfIsForbidden
);
let issuer = Smiths::<T>::get(issuer_index).ok_or(Error::<T>::OriginHasNeverBeenInvited)?;
ensure!(
issuer.status == SmithStatus::Smith,
Error::<T>::CertificationIsASmithPrivilege
);
ensure!(
issuer.expires_on.is_none(),
Error::<T>::CertificationIsAOnlineSmithPrivilege
);
let issued_certs = issuer.issued_certs.len();
ensure!(
issued_certs < T::MaxByIssuer::get() as usize,
Error::<T>::CertificationStockFullyConsumed
);
let receiver = Smiths::<T>::get(receiver_index)
.ok_or(Error::<T>::CertificationReceiverMustHaveBeenInvited)?;
ensure!(
receiver.status != SmithStatus::Invited,
Error::<T>::CertificationMustBeAgreed
);
ensure!(
receiver.status != SmithStatus::Excluded,
Error::<T>::CertificationOnExcludedIsForbidden
);
ensure!(
receiver
.received_certs
.binary_search(&issuer_index)
.is_err(),
Error::<T>::CertificationAlreadyExists
);
Ok(().into())
}
/// Perform certification of a potential Smith by another Smith.
fn do_certify_smith(receiver: T::IdtyIndex, issuer: T::IdtyIndex) {
// - adds a certification in issuer issued list
Smiths::<T>::mutate(issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.issued_certs.push(receiver);
smith_meta.issued_certs.sort();
}
});
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
// - adds a certification in receiver received list
smith_meta.received_certs.push(issuer);
smith_meta.received_certs.sort();
Self::deposit_event(Event::<T>::SmithCertAdded { issuer, receiver });
// - receiving a certification either lead us to Pending or Smith status
let previous_status = smith_meta.status;
smith_meta.status =
if smith_meta.received_certs.len() >= T::MinCertForMembership::get() as usize {
// - if the number of certification received by the receiver is enough, win the Smith status (or keep it)
SmithStatus::Smith
} else {
// - otherwise we are (still) a pending smith
SmithStatus::Pending
};
if previous_status != SmithStatus::Smith {
// - postpone the expiration: a Pending smith cannot do anything but wait
// this postponement is here to ease the process of becoming a smith
let new_expires_on =
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
smith_meta.expires_on = Some(new_expires_on);
ExpiresOn::<T>::append(new_expires_on, receiver);
}
// - if the status is smith but wasn't, notify that smith gained membership
if smith_meta.status == SmithStatus::Smith && previous_status != SmithStatus::Smith
{
Self::deposit_event(Event::<T>::SmithMembershipAdded {
idty_index: receiver,
});
}
// TODO: (optimization) unschedule old expiry
}
});
}
/// Handle the removal of Smiths whose expiration time has been reached at a given session index.
fn on_exclude_expired_smiths(at: SessionIndex) {
if let Some(smiths_to_remove) = ExpiresOn::<T>::take(at) {
for smith in smiths_to_remove {
if let Some(smith_meta) = Smiths::<T>::get(smith) {
if let Some(expires_on) = smith_meta.expires_on {
if expires_on == at {
Self::_do_exclude_smith(smith, SmithRemovalReason::OfflineTooLong);
}
}
}
}
}
}
/// Handle actions upon the removal of a Web of Trust member.
pub fn on_removed_wot_member(idty_index: T::IdtyIndex) -> Weight {
let mut weight = T::WeightInfo::on_removed_wot_member_empty();
if Smiths::<T>::get(idty_index).is_some() {
Self::_do_exclude_smith(idty_index, SmithRemovalReason::LostMembership);
weight = weight.saturating_add(T::WeightInfo::on_removed_wot_member());
}
weight
}
/// Perform the exclusion of a Smith.
fn _do_exclude_smith(receiver: T::IdtyIndex, reason: SmithRemovalReason) {
let mut lost_certs = vec![];
Smiths::<T>::mutate(receiver, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
smith_meta.expires_on = None;
smith_meta.status = SmithStatus::Excluded;
for cert in &smith_meta.received_certs {
lost_certs.push(*cert);
}
smith_meta.received_certs = vec![];
// N.B.: the issued certs are kept in case the smith joins back
}
});
// We remove the lost certs from their issuer's stock
for lost_cert_issuer in lost_certs {
Smiths::<T>::mutate(lost_cert_issuer, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
if let Ok(index) = smith_meta.issued_certs.binary_search(&receiver) {
smith_meta.issued_certs.remove(index);
Self::deposit_event(Event::<T>::SmithCertRemoved {
issuer: lost_cert_issuer,
receiver,
});
}
}
});
}
// Deletion done: notify (authority-members) for cascading
T::OnSmithDelete::on_smith_delete(receiver, reason);
Self::deposit_event(Event::<T>::SmithMembershipRemoved {
idty_index: receiver,
});
}
/// Handle the event when a Smith goes online.
pub fn on_smith_goes_online(idty_index: T::IdtyIndex) {
Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
if smith_meta.expires_on.is_some() {
// As long as the smith is online, it cannot expire
smith_meta.expires_on = None;
smith_meta.last_online = None;
}
}
});
}
/// Handle the event when a Smith goes offline.
pub fn on_smith_goes_offline(idty_index: T::IdtyIndex) {
Smiths::<T>::mutate(idty_index, |maybe_smith_meta| {
if let Some(smith_meta) = maybe_smith_meta {
// Smith can go offline after main membership expiry
// in this case, there is no scheduled expiry since it is already excluded
if smith_meta.status != SmithStatus::Excluded {
// schedule expiry
let new_expires_on =
CurrentSession::<T>::get() + T::SmithInactivityMaxDuration::get();
smith_meta.expires_on = Some(new_expires_on);
let block_number = frame_system::pallet::Pallet::<T>::block_number();
smith_meta.last_online = Some(block_number);
ExpiresOn::<T>::append(new_expires_on, idty_index);
}
}
});
}
/// Provide whether the given identity index is a Smith.
fn provide_is_member(idty_id: &T::IdtyIndex) -> bool {
Smiths::<T>::get(idty_id).map_or(false, |smith| smith.status == SmithStatus::Smith)
}
}
impl<T: Config> sp_runtime::traits::IsMember<T::IdtyIndex> for Pallet<T> {
fn is_member(idty_id: &T::IdtyIndex) -> bool {
Self::provide_is_member(idty_id)
}
}
// Copyright 2021-2023 Axiom-Team
//
// 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/>.
use crate::{self as pallet_smith_members};
use frame_support::{
derive_impl,
pallet_prelude::Hooks,
parameter_types,
traits::{ConstU32, ConstU64},
weights::{constants::RocksDbWeight, Weight},
};
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, ConvertInto, IdentityLookup, IsMember},
BuildStorage, Perbill,
};
parameter_types! {
pub static OnOffencePerbill: Vec<Perbill> = Default::default();
pub static OffenceWeight: Weight = Default::default();
}
type Block = frame_system::mocking::MockBlock<Runtime>;
frame_support::construct_runtime!(
pub struct Runtime {
System: frame_system,
Smith: pallet_smith_members,
}
);
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Runtime {
type AccountId = u64;
type BaseCallFilter = frame_support::traits::Everything;
type Block = Block;
type BlockHashCount = ConstU64<250>;
type DbWeight = RocksDbWeight;
type Hash = H256;
type Hashing = BlakeTwo256;
type Lookup = IdentityLookup<Self::AccountId>;
type MaxConsumers = ConstU32<16>;
type Nonce = u64;
type PalletInfo = PalletInfo;
type RuntimeCall = RuntimeCall;
type RuntimeEvent = RuntimeEvent;
type RuntimeOrigin = RuntimeOrigin;
}
pub struct EveryoneExceptIdZero;
impl IsMember<u64> for EveryoneExceptIdZero {
fn is_member(member_id: &u64) -> bool {
member_id != &0 && member_id != &10
}
}
impl pallet_smith_members::Config for Runtime {
type IdtyAttr = ();
type IdtyIdOfAuthorityId = ConvertInto;
type IdtyIndex = u64;
type IsWoTMember = EveryoneExceptIdZero;
type MaxByIssuer = ConstU32<3>;
type MemberId = u64;
type MinCertForMembership = ConstU32<2>;
type OnSmithDelete = ();
type RuntimeEvent = RuntimeEvent;
type SmithInactivityMaxDuration = ConstU32<5>;
type WeightInfo = ();
}
pub fn new_test_ext(
genesis_config: crate::pallet::GenesisConfig<Runtime>,
) -> sp_io::TestExternalities {
RuntimeGenesisConfig {
system: SystemConfig::default(),
smith: genesis_config,
}
.build_storage()
.unwrap()
.into()
}
pub fn run_to_block(n: u64) {
while System::block_number() < n {
Smith::on_finalize(System::block_number());
System::on_finalize(System::block_number());
System::reset_events();
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
Smith::on_initialize(System::block_number());
}
}
// Copyright 2021-2023 Axiom-Team
//
// 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/>.
use super::*;
use crate::mock::{new_test_ext, run_to_block, Runtime, RuntimeEvent, RuntimeOrigin, System};
use frame_support::{assert_err, assert_ok};
use crate::SmithStatus::{Excluded, Invited, Pending, Smith};
#[cfg(test)]
use maplit::btreemap;
use pallet_authority_members::OnNewSession;
#[test]
fn process_to_become_a_smith_and_lose_it() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
// Events cannot be recorded on genesis
run_to_block(1);
// State before
assert_eq!(Smiths::<Runtime>::get(5), None);
// Try to invite
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::InvitationSent {
receiver: 5,
issuer: 1,
}));
// Accept invitation
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::InvitationAccepted {
idty_index: 5,
}));
// State after
assert_eq!(
Smiths::<Runtime>::get(5).unwrap(),
SmithMeta {
status: SmithStatus::Pending,
expires_on: Some(5),
issued_certs: vec![],
received_certs: vec![],
last_online: None,
}
);
// Then certification 1/2
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertAdded {
receiver: 5,
issuer: 1,
}));
assert_eq!(
Smiths::<Runtime>::get(5).unwrap(),
SmithMeta {
status: SmithStatus::Pending,
expires_on: Some(5),
issued_certs: vec![],
received_certs: vec![1],
last_online: None,
}
);
// Then certification 2/2
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertAdded {
receiver: 5,
issuer: 1,
}));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipAdded { idty_index: 5 },
));
assert_eq!(
Smiths::<Runtime>::get(5).unwrap(),
SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(5),
issued_certs: vec![],
received_certs: vec![1, 2],
last_online: None,
}
);
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_offline(1);
Pallet::<Runtime>::on_smith_goes_offline(2);
// On session 4 everything if fine
Pallet::<Runtime>::on_new_session(4);
assert!(Smiths::<Runtime>::get(1).is_some());
assert!(Smiths::<Runtime>::get(2).is_some());
assert!(Smiths::<Runtime>::get(5).is_some());
// On session 5 no more smiths because of lack of activity
Pallet::<Runtime>::on_new_session(5);
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipRemoved { idty_index: 1 },
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 2,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 3,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 4,
}));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipRemoved { idty_index: 2 },
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 2,
issuer: 3,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 2,
issuer: 4,
}));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipRemoved { idty_index: 5 },
));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 1,
issuer: 3,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 5,
issuer: 1,
}));
System::assert_has_event(RuntimeEvent::Smith(Event::<Runtime>::SmithCertRemoved {
receiver: 5,
issuer: 2,
}));
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: Some(1),
})
);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: Some(1),
})
);
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: None,
})
);
});
}
#[test]
fn avoid_multiple_events_for_becoming_smith() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_smith_goes_online(3);
// Events cannot be recorded on genesis
run_to_block(1);
// State before
assert_eq!(Smiths::<Runtime>::get(5), None);
// Try to invite
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
System::assert_has_event(RuntimeEvent::Smith(
Event::<Runtime>::SmithMembershipAdded { idty_index: 5 },
));
run_to_block(2);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
5
));
// Should not be promoted again
assert!(!System::events().iter().any(|record| record.event
== RuntimeEvent::Smith(Event::<Runtime>::SmithMembershipAdded { idty_index: 5 },)));
});
}
#[test]
fn should_have_checks_on_certify() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![4]),
4 => (false, vec![1, 2]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
// Initially
assert_eq!(
Smiths::<Runtime>::get(1).unwrap(),
SmithMeta {
status: Smith,
expires_on: None,
issued_certs: vec![4],
received_certs: vec![2, 3, 4],
last_online: None,
}
);
assert_eq!(
Smiths::<Runtime>::get(2).unwrap(),
SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![1, 4],
received_certs: vec![3, 4],
last_online: None,
}
);
assert_eq!(
Smiths::<Runtime>::get(3).unwrap(),
SmithMeta {
status: Pending,
expires_on: Some(5),
issued_certs: vec![1, 2],
received_certs: vec![4],
last_online: None,
}
);
assert_eq!(
Smiths::<Runtime>::get(4).unwrap(),
SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![1, 2, 3],
received_certs: vec![1, 2],
last_online: None,
}
);
// Tries all possible errors
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(0), 1),
Error::<Runtime>::OriginHasNeverBeenInvited
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 1),
Error::<Runtime>::CertificationOfSelfIsForbidden
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(3), 5),
Error::<Runtime>::CertificationIsASmithPrivilege
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 6),
Error::<Runtime>::CertificationReceiverMustHaveBeenInvited
);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 4),
Error::<Runtime>::CertificationAlreadyExists
);
// #3: state before
assert_eq!(
Smiths::<Runtime>::get(3).unwrap(),
SmithMeta {
status: Pending,
expires_on: Some(5),
issued_certs: vec![1, 2],
received_certs: vec![4],
last_online: None,
}
);
// Try to certify #3
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
3
));
// #3: state after
assert_eq!(
Smiths::<Runtime>::get(3).unwrap(),
SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(5),
issued_certs: vec![1, 2],
received_certs: vec![1, 4],
last_online: None,
}
);
});
}
#[test]
fn smith_activity_postpones_expiration() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![]),
4 => (false, vec![])
],
})
.execute_with(|| {
// On session 4 everything is fine
Pallet::<Runtime>::on_new_session(4);
assert!(Smiths::<Runtime>::get(1).is_some());
assert!(Smiths::<Runtime>::get(2).is_some());
// Smith #2 is online but not #1
Pallet::<Runtime>::on_smith_goes_online(2);
// On session 5: exclusion for lack of activity
Pallet::<Runtime>::on_new_session(5);
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: None,
})
);
// issued_certs is empty because #1 was excluded
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Smith,
expires_on: None,
issued_certs: vec![],
received_certs: vec![3, 4],
last_online: None,
})
);
// Smith #2 goes offline
Pallet::<Runtime>::on_new_session(6);
Pallet::<Runtime>::on_smith_goes_offline(2);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(11),
issued_certs: vec![],
received_certs: vec![3, 4],
last_online: Some(0),
})
);
// Still not expired on session 10
Pallet::<Runtime>::on_new_session(10);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Smith,
expires_on: Some(11),
issued_certs: vec![],
received_certs: vec![3, 4],
last_online: Some(0),
})
);
// But expired on session 11
Pallet::<Runtime>::on_new_session(11);
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: None,
})
);
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: vec![],
received_certs: vec![],
last_online: Some(0),
})
);
});
}
#[test]
fn smith_coming_back_recovers_its_issued_certs() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 4]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Not activity for Smith #2
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(3);
Pallet::<Runtime>::on_smith_goes_online(4);
// Smith #2 gets excluded
Pallet::<Runtime>::on_new_session(5);
// The issued certs are preserved
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: Excluded,
expires_on: None,
issued_certs: vec![1],
received_certs: vec![],
last_online: None,
})
);
// Smith #2 comes back
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 2));
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
2
)));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
2
));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
2
));
// Smith #2 is back with its issued certs recovered, but not its received certs
assert_eq!(
Smiths::<Runtime>::get(2),
Some(SmithMeta {
status: Smith,
expires_on: Some(10),
issued_certs: vec![1],
received_certs: vec![1, 3],
last_online: None,
})
);
Pallet::<Runtime>::on_smith_goes_online(2);
// We can verify it with the stock rule
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
3
));
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
4
));
// Max stock is reached (3 = 1 recovered + 2 new)
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(2), 5),
Error::<Runtime>::CertificationStockFullyConsumed
);
});
}
#[test]
fn certifying_on_different_status() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_smith_goes_online(3);
// State before
assert_eq!(Smiths::<Runtime>::get(5), None);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 5),
Error::<Runtime>::CertificationReceiverMustHaveBeenInvited
);
// After invitation
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Invited);
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 5),
Error::<Runtime>::CertificationMustBeAgreed
);
// After acceptation
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Pending);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Pending);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Smith);
// After being a smith
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
5
));
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_new_session(5);
assert_eq!(Smiths::<Runtime>::get(1).unwrap().status, Smith);
assert_eq!(Smiths::<Runtime>::get(2).unwrap().status, Smith);
assert_eq!(Smiths::<Runtime>::get(5).unwrap().status, Excluded);
// After being excluded
assert_err!(
Pallet::<Runtime>::certify_smith(RuntimeOrigin::signed(1), 5),
Error::<Runtime>::CertificationOnExcludedIsForbidden
);
});
}
#[test]
fn certifying_an_online_smith() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
Pallet::<Runtime>::on_smith_goes_online(2);
Pallet::<Runtime>::on_smith_goes_online(3);
assert_ok!(Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 5));
assert_ok!(Pallet::<Runtime>::accept_invitation(RuntimeOrigin::signed(
5
)));
Pallet::<Runtime>::on_new_session(2);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(1),
5
));
Pallet::<Runtime>::on_new_session(3);
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(2),
5
));
// Smith can expire
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: Smith,
expires_on: Some(8),
issued_certs: vec![],
received_certs: vec![1, 2],
last_online: None,
})
);
assert_eq!(ExpiresOn::<Runtime>::get(7), Some(vec![5]));
assert_eq!(ExpiresOn::<Runtime>::get(8), Some(vec![5]));
Pallet::<Runtime>::on_smith_goes_online(5);
// After going online, the expiration disappears
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: Smith,
expires_on: None,
issued_certs: vec![],
received_certs: vec![1, 2],
last_online: None,
})
);
// ExpiresOn is not unscheduled, but as expires_on has switched to None it's not a problem
assert_eq!(ExpiresOn::<Runtime>::get(7), Some(vec![5]));
assert_eq!(ExpiresOn::<Runtime>::get(8), Some(vec![5]));
// We can receive certification without postponing the expiration (because we are online)
assert_ok!(Pallet::<Runtime>::certify_smith(
RuntimeOrigin::signed(3),
5
));
assert_eq!(
Smiths::<Runtime>::get(5),
Some(SmithMeta {
status: Smith,
expires_on: None,
issued_certs: vec![],
received_certs: vec![1, 2, 3],
last_online: None,
})
);
});
}
/// Test that scheduled expiration is removed after session
#[test]
fn expires_on_cleans_up() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (true, vec![2, 3]),
2 => (true, vec![1,3]),
3 => (true, vec![1,2]),
],
})
.execute_with(|| {
// Alice goes offline, and is set to expire
Pallet::<Runtime>::on_smith_goes_offline(1);
// The expiration block is present in smith data
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![2, 3],
received_certs: vec![2, 3],
last_online: Some(0),
})
);
// It is also present in ExpiresOn schedule
assert_eq!(ExpiresOn::<Runtime>::get(5), Some(vec![1]));
// Go to expiration session
Pallet::<Runtime>::on_new_session(5);
// Alice is expired
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Excluded,
expires_on: None,
issued_certs: vec![2, 3],
received_certs: vec![],
last_online: Some(0),
})
);
// ExpiresOn is clean
assert_eq!(ExpiresOn::<Runtime>::get(5), None);
});
}
#[test]
fn invitation_on_non_wot_member() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// Go online to be able to invite+certify
Pallet::<Runtime>::on_smith_goes_online(1);
// State before
assert_eq!(Smiths::<Runtime>::get(10), None);
// After invitation
assert_err!(
Pallet::<Runtime>::invite_smith(RuntimeOrigin::signed(1), 10),
Error::<Runtime>::InvitationOfNonMember
);
});
}
#[test]
fn losing_wot_membership_cascades_to_smith_members() {
new_test_ext(GenesisConfig {
initial_smiths: btreemap![
1 => (false, vec![2, 3, 4]),
2 => (false, vec![3, 4]),
3 => (false, vec![1, 2]),
4 => (false, vec![]),
],
})
.execute_with(|| {
// State before
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Smith,
expires_on: Some(5),
issued_certs: vec![3],
received_certs: vec![2, 3, 4],
last_online: None,
})
);
assert_eq!(
Smiths::<Runtime>::get(1).unwrap().issued_certs,
Vec::<u64>::from([3])
);
assert_eq!(
Smiths::<Runtime>::get(2).unwrap().issued_certs,
Vec::<u64>::from([1, 3])
);
assert_eq!(
Smiths::<Runtime>::get(3).unwrap().issued_certs,
Vec::<u64>::from([1, 2])
);
assert_eq!(
Smiths::<Runtime>::get(4).unwrap().issued_certs,
Vec::<u64>::from([1, 2])
);
Pallet::<Runtime>::on_removed_wot_member(1);
// Excluded
assert_eq!(
Smiths::<Runtime>::get(1),
Some(SmithMeta {
status: Excluded,
expires_on: None,
issued_certs: vec![3],
received_certs: vec![],
last_online: None,
})
);
// Issued certifications updated for certifiers of 1
assert_eq!(
Smiths::<Runtime>::get(1).unwrap().issued_certs,
Vec::<u64>::from([3])
);
assert_eq!(
Smiths::<Runtime>::get(2).unwrap().issued_certs,
Vec::<u64>::from([3])
);
assert_eq!(
Smiths::<Runtime>::get(3).unwrap().issued_certs,
Vec::<u64>::from([2])
);
assert_eq!(
Smiths::<Runtime>::get(4).unwrap().issued_certs,
Vec::<u64>::from([2])
);
});
}
use crate::SmithRemovalReason;
/// Trait for handling actions when a Smith is deleted.
pub trait OnSmithDelete<IdtyIndex> {
/// Handle the deletion of a smith.
fn on_smith_delete(idty_index: IdtyIndex, reason: SmithRemovalReason);
}
impl<IdtyIndex> OnSmithDelete<IdtyIndex> for () {
fn on_smith_delete(_: IdtyIndex, _: SmithRemovalReason) {}
}
// Copyright 2021 Axiom-Team
//
// 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/>.
//! Various basic types for use in the identity pallet.
use crate::SmithStatus;
use codec::{Decode, Encode};
use frame_support::pallet_prelude::*;
use scale_info::{prelude::vec::Vec, TypeInfo};
use sp_staking::SessionIndex;
/// Represents a certification metadata attached to a Smith identity.
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct SmithMeta<IdtyIndex, BlockNumber> {
/// Current status of the Smith.
pub status: SmithStatus,
/// The session at which the Smith will expire (for lack of validation activity).
pub expires_on: Option<SessionIndex>,
/// Certifications issued to other Smiths.
pub issued_certs: Vec<IdtyIndex>,
/// Certifications received from other Smiths.
pub received_certs: Vec<IdtyIndex>,
/// Last online time.
pub last_online: Option<BlockNumber>,
}
/// By default, a smith has the least possible privileges
impl<IdtyIndex, BlockNumber> Default for SmithMeta<IdtyIndex, BlockNumber> {
fn default() -> Self {
Self {
status: SmithStatus::Excluded,
expires_on: None,
issued_certs: Vec::<IdtyIndex>::new(),
received_certs: Vec::<IdtyIndex>::new(),
last_online: Default::default(),
}
}
}
// Copyright 2021-2022 Axiom-Team // Copyright 2021-2023 Axiom-Team
// //
// This file is part of Duniter-v2S. // This file is part of Duniter-v2S.
// //
...@@ -14,32 +14,37 @@ ...@@ -14,32 +14,37 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::*; #![allow(clippy::unnecessary_cast)]
pub struct MigrationsV400; use frame_support::weights::Weight;
impl frame_support::traits::OnRuntimeUpgrade for MigrationsV400 {
fn on_runtime_upgrade() -> Weight {
let mut weight = Weight::from_ref_time(1_000_000_000); // Safety margin
type OldvalueType = AccountId; /// Weight functions needed for pallet.
pub trait WeightInfo {
fn invite_smith() -> Weight;
fn accept_invitation() -> Weight;
fn certify_smith() -> Weight;
fn on_removed_wot_member() -> Weight;
fn on_removed_wot_member_empty() -> Weight;
}
impl WeightInfo for () {
fn invite_smith() -> Weight {
Weight::zero()
}
pallet_membership::PendingMembership::<Runtime, Instance1>::translate_values( fn accept_invitation() -> Weight {
|_: OldvalueType| { Weight::zero()
*weight.ref_time_mut() += <Runtime as frame_system::Config>::DbWeight::get().write; }
Some(())
},
);
weight fn certify_smith() -> Weight {
Weight::zero()
} }
#[cfg(feature = "try-runtime")] fn on_removed_wot_member() -> Weight {
fn pre_upgrade() -> Result<frame_benchmarking::Vec<u8>, &'static str> { Weight::zero()
Ok(Vec::new())
} }
#[cfg(feature = "try-runtime")] fn on_removed_wot_member_empty() -> Weight {
fn post_upgrade(_state: frame_benchmarking::Vec<u8>) -> Result<(), &'static str> { Weight::zero()
Ok(())
} }
} }
[package] [package]
authors = ['librelois <c@elo.tf>'] authors.workspace = true
description = 'FRAME pallet universal dividend.' description = "duniter pallet universal dividend"
edition = "2021" edition.workspace = true
homepage = 'https://duniter.org' homepage.workspace = true
license = 'AGPL-3.0' license.workspace = true
name = 'pallet-universal-dividend' name = "pallet-universal-dividend"
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' repository.workspace = true
version = '3.0.0' version.workspace = true
[features] [features]
default = ['std'] default = ["std"]
runtime-benchmarks = [ runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks", "frame-benchmarking/runtime-benchmarks",
"pallet-balances", "frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-balances/runtime-benchmarks",
"pallet-timestamp/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"pallet-balances/runtime-benchmarks",
"pallet-balances/try-runtime",
"pallet-timestamp/runtime-benchmarks",
"pallet-timestamp/try-runtime",
"sp-runtime/try-runtime",
] ]
std = [ std = [
'codec/std', "codec/std",
'frame-support/std', "frame-benchmarking?/std",
'frame-system/std', "frame-support/std",
'frame-benchmarking/std', "frame-system/std",
"serde", "pallet-balances/std",
"pallet-timestamp/std",
"scale-info/std",
"serde/std",
"sp-api/std",
"sp-arithmetic/std", "sp-arithmetic/std",
"sp-core/std",
"sp-io/std", "sp-io/std",
"sp-std/std", "sp-runtime/std",
] ]
try-runtime = ['frame-support/try-runtime']
[dependencies]
# crates.io
codec = { package = 'parity-scale-codec', version = "3.1.5", default-features = false, features = ["derive", "max-encoded-len"] }
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
serde = { version = "1.0.101", features = ["derive"], optional = true }
# substrate bencharks
frame-benchmarking = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', optional = true, default-features = false }
pallet-balances = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32', optional = true, default-features = false }
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-system]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-arithmetic]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-io]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
### DOC ###
[package.metadata.docs.rs] [package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu'] default-features = false
targets = ["x86_64-unknown-linux-gnu"]
### DEV ### [dependencies]
duniter-primitives = { workspace = true }
codec = { workspace = true, features = ["derive", "max-encoded-len"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
pallet-balances = { workspace = true }
pallet-timestamp = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
sp-api = { workspace = true }
sp-arithmetic = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies] [dev-dependencies]
serde = { version = "1.0.101", features = ["derive"] } sp-core = { workspace = true, default-features = true }
[dev-dependencies.pallet-balances]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dev-dependencies.sp-core]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dev-dependencies.sp-io]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dev-dependencies.sp-runtime]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
# Duniter universal dividend pallet
One of the main features of Duniter is the Universal Dividend based on the Relative Theory of Money. It is both a daily monetary creation and a measure unit.
This pallet provides functions to create UDs and transfer an amount of currency counted in UD. It should be noted that the UD is not actually created every day on every account which would be very resource consuming but must be claimed by the member in a given extrinsic.
\ No newline at end of file
...@@ -15,147 +15,158 @@ ...@@ -15,147 +15,158 @@
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg(feature = "runtime-benchmarks")] #![cfg(feature = "runtime-benchmarks")]
#![allow(clippy::multiple_bound_locations)]
use super::*; use super::*;
use core::num::NonZeroU16;
use frame_benchmarking::{account, benchmarks, whitelist_account, whitelisted_caller}; use frame_benchmarking::{account, v2::*, whitelisted_caller};
use frame_support::pallet_prelude::{BoundedVec, IsType}; use frame_support::{pallet_prelude::IsType, traits::StoredMap};
use frame_support::traits::{Get, OnInitialize};
use frame_system::RawOrigin; use frame_system::RawOrigin;
use pallet_balances::Pallet as Balances; use pallet_balances::Pallet as Balances;
use sp_runtime::traits::Bounded;
use crate::Pallet; use crate::Pallet;
const ED_MULTIPLIER: u32 = 10; const ED_MULTIPLIER: u32 = 10;
const SEED: u32 = 0;
benchmarks! { #[benchmarks(
where_clause {
where
T: pallet_balances::Config, T::Balance: From<u64>,
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>
}
on_initialize {
let total_money_created = Pallet::<T>::total_money_created();
}: { Pallet::<T>::on_initialize(1_u32.into()); }
verify {
assert_eq!(Pallet::<T>::total_money_created(), total_money_created);
}
where_clause {
where
T: pallet_balances::Config, T::Balance: From<u64>,
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>
}
on_initialize_ud_created {
let block_number = T::UdCreationPeriod::get();
let block_number_plus_one: T::BlockNumber = block_number + One::one();
NextReeval::<T>::put(block_number_plus_one);
}: { Pallet::<T>::on_initialize(block_number); }
verify {
}
where_clause {
where
T: pallet_balances::Config, T::Balance: From<u64>,
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance>
}
on_initialize_ud_reevalued {
let block_number = T::UdCreationPeriod::get();
let block_number_plus_one: T::BlockNumber = block_number + One::one();
NextReeval::<T>::put(block_number_plus_one);
Pallet::<T>::on_initialize(block_number);
NextReeval::<T>::put(block_number);
}: { Pallet::<T>::on_initialize(block_number); }
verify {
}
// Benchmark `claim_uds` extrinsic with the worst possible conditions:
// * UDs have never been claimed
// * The maximum number of revaluations has taken place since
where_clause {
where where
T: pallet_balances::Config, T::Balance: From<u64>, T: pallet_balances::Config, T::Balance: From<u64>,
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance> BalanceOf<T>: IsType<T::Balance>
} )]
claim_uds { mod benchmarks {
let n in 1 .. T::MaxPastReeval::get(); use super::*;
// Caller should be a member
let caller: T::AccountId = T::MembersStorageIter::from(None)
.next()
.expect("we need at least one member")
.0;
// Simulate n reevals
let mut past_reevals = BoundedVec::default();
for i in 0..n {
past_reevals
.try_push((((3 * i) + 1) as u16, (1_000 + (100 * i)).into()))
.expect("unreachable claim");
}
PastReevals::<T>::put(past_reevals);
// Simulate 3n+2 UDs
CurrentUdIndex::<T>::put(((3 * n) + 2) as u16);
whitelist_account!(caller); fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
}: claim_uds(RawOrigin::Signed(caller)) frame_system::Pallet::<T>::assert_has_event(generic_event.into());
verify {
} }
#[benchmark]
fn claim_uds(i: Linear<1, { T::MaxPastReeval::get() }>) -> Result<(), BenchmarkError> {
// Benchmark `transfer_ud` extrinsic with the worst possible conditions: // Benchmark `transfer_ud` extrinsic with the worst possible conditions:
// * Transfer will kill the sender account. // * Transfer will kill the sender account.
// * Transfer will create the recipient account. // * Transfer will create the recipient account.
where_clause { let caller: T::AccountId = T::IdtyAttr::owner_key(1).unwrap();
where CurrentUdIndex::<T>::put(2054u16);
T: pallet_balances::Config, T::Balance: From<u64>, T::MembersStorage::insert(
<T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance> &caller,
FirstEligibleUd(Some(
NonZeroU16::new(CurrentUdIndex::<T>::get() - i as u16).unwrap(),
)),
)?;
let (_, uds_total) = compute_claim_uds::compute_claim_uds(
CurrentUdIndex::<T>::get(),
CurrentUdIndex::<T>::get() - i as u16,
PastReevals::<T>::get().into_iter(),
);
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert_has_event::<T>(
Event::<T>::UdsClaimed {
count: i as u16,
total: uds_total,
who: caller,
} }
transfer_ud { .into(),
);
Ok(())
}
#[benchmark]
fn transfer_ud() {
let existential_deposit = T::ExistentialDeposit::get(); let existential_deposit = T::ExistentialDeposit::get();
let caller = whitelisted_caller(); let caller = whitelisted_caller();
// Give some multiple of the existential deposit
let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
let _ = T::Currency::make_free_balance_be(&caller, balance.into()); let _ = T::Currency::set_balance(&caller, balance.into());
// Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account and reap this user.
// Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, let recipient: T::AccountId = account("recipient", 0, 1);
// and reap this user. let recipient_lookup: <T::Lookup as StaticLookup>::Source =
let recipient: T::AccountId = account("recipient", 0, SEED); T::Lookup::unlookup(recipient.clone());
let recipient_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(recipient.clone()); let transfer_amount =
let transfer_amount = existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into(); existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into();
let transfer_amount_ud = transfer_amount.saturating_mul(1_000.into()) / Pallet::<T>::current_ud().into(); let transfer_amount_ud =
}: _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount_ud.into()) transfer_amount.saturating_mul(1_000.into()) / Pallet::<T>::current_ud().into();
verify {
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()),
recipient_lookup,
transfer_amount_ud.into(),
);
assert_eq!(Balances::<T>::free_balance(&caller), Zero::zero()); assert_eq!(Balances::<T>::free_balance(&caller), Zero::zero());
assert_eq!(Balances::<T>::free_balance(&recipient), transfer_amount); assert_eq!(Balances::<T>::free_balance(&recipient), transfer_amount);
} }
#[benchmark]
fn transfer_ud_keep_alive() {
// Benchmark `transfer_ud_keep_alive` with the worst possible condition: // Benchmark `transfer_ud_keep_alive` with the worst possible condition:
// * The recipient account is created. // * The recipient account is created.
where_clause { where T: pallet_balances::Config, T::Balance: From<u64>, <T::Currency as Currency<T::AccountId>>::Balance: IsType<T::Balance> }
transfer_ud_keep_alive {
let caller = whitelisted_caller(); let caller = whitelisted_caller();
let recipient: T::AccountId = account("recipient", 0, SEED); let recipient: T::AccountId = account("recipient", 0, 1);
let recipient_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(recipient.clone()); let recipient_lookup: <T::Lookup as StaticLookup>::Source =
T::Lookup::unlookup(recipient.clone());
// Give the sender account max funds, thus a transfer will not kill account. // Give the sender account max funds, thus a transfer will not kill account.
let _ = T::Currency::make_free_balance_be(&caller, <T::Currency as Currency<T::AccountId>>::Balance::max_value()); let _ = T::Currency::set_balance(&caller, u32::MAX.into());
let existential_deposit = T::ExistentialDeposit::get(); let existential_deposit = T::ExistentialDeposit::get();
let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into()); let transfer_amount = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
let transfer_amount_ud = transfer_amount.saturating_mul(1_000.into()) / Pallet::<T>::current_ud().into(); let transfer_amount_ud =
}: _(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount_ud.into()) transfer_amount.saturating_mul(1_000.into()) / Pallet::<T>::current_ud().into();
verify {
#[extrinsic_call]
_(
RawOrigin::Signed(caller.clone()),
recipient_lookup,
transfer_amount_ud.into(),
);
assert!(!Balances::<T>::free_balance(&caller).is_zero()); assert!(!Balances::<T>::free_balance(&caller).is_zero());
assert_eq!(Balances::<T>::free_balance(&recipient), transfer_amount); assert_eq!(Balances::<T>::free_balance(&recipient), transfer_amount);
} }
#[benchmark]
fn on_removed_member(i: Linear<1, { T::MaxPastReeval::get() }>) -> Result<(), BenchmarkError> {
let caller: T::AccountId = T::IdtyAttr::owner_key(1).unwrap();
CurrentUdIndex::<T>::put(2054u16);
T::MembersStorage::insert(
&caller,
FirstEligibleUd(Some(
NonZeroU16::new(CurrentUdIndex::<T>::get() - i as u16).unwrap(),
)),
)?;
let (_, uds_total) = compute_claim_uds::compute_claim_uds(
CurrentUdIndex::<T>::get(),
CurrentUdIndex::<T>::get() - i as u16,
PastReevals::<T>::get().into_iter(),
);
#[block]
{
Pallet::<T>::on_removed_member(CurrentUdIndex::<T>::get() - i as u16, &caller);
}
if i != 0 {
assert_has_event::<T>(
Event::<T>::UdsAutoPaid {
count: i as u16,
total: uds_total,
who: caller,
}
.into(),
);
}
Ok(())
}
impl_benchmark_test_suite!( impl_benchmark_test_suite!(
Pallet, Pallet,
crate::mock::new_test_ext(crate::mock::UniversalDividendConfig { crate::mock::new_test_ext(crate::mock::UniversalDividendConfig {
first_reeval: 8, first_reeval: Some(48_000),
first_ud: 1_000, first_ud: Some(6_000),
initial_monetary_mass: 0, initial_monetary_mass: 0,
initial_members: vec![1], initial_members: vec![1],
ud: 10,
}), }),
crate::mock::Test crate::mock::Test
); );
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::UdIndex; use super::UdIndex;
use core::iter::DoubleEndedIterator;
use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero}; use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero};
pub(super) fn compute_claim_uds<Balance: AtLeast32BitUnsigned>( pub(super) fn compute_claim_uds<Balance: AtLeast32BitUnsigned>(
...@@ -25,17 +24,22 @@ pub(super) fn compute_claim_uds<Balance: AtLeast32BitUnsigned>( ...@@ -25,17 +24,22 @@ pub(super) fn compute_claim_uds<Balance: AtLeast32BitUnsigned>(
) -> (UdIndex, Balance) { ) -> (UdIndex, Balance) {
let mut total_amount = Zero::zero(); let mut total_amount = Zero::zero();
let mut total_count = 0; let mut total_count = 0;
for (ud_index, ud_amount) in past_reevals.rev() { // We start in reverse order, i.e. the most recent reeval first
if ud_index <= first_ud_index { for (reeval_index, ud_amount) in past_reevals.rev() {
// Therefore, if our first UD is above the current reeval index, we have reached our final useful reeval and must break
if reeval_index <= first_ud_index {
let count = current_ud_index - first_ud_index; let count = current_ud_index - first_ud_index;
total_amount += Balance::from(count) * ud_amount; total_amount += Balance::from(count) * ud_amount;
total_count += count; total_count += count;
// First unclaimed UD is reached; stop counting now.
break; break;
} else { }
let count = current_ud_index - ud_index; // Otherwise, we consume the full reeval contained UDs
else {
let count = current_ud_index - reeval_index;
total_amount += Balance::from(count) * ud_amount; total_amount += Balance::from(count) * ud_amount;
total_count += count; total_count += count;
current_ud_index = ud_index; current_ud_index = reeval_index;
} }
} }
...@@ -109,4 +113,19 @@ mod tests { ...@@ -109,4 +113,19 @@ mod tests {
(2, 110_000) (2, 110_000)
); );
} }
#[test]
fn very_old_unclaimed_ud_out_of_reevals() {
let past_reevals = vec![
// (3, 100 as Balance), "old" reeval which has gone out of reevals window.
(4, 1_000 as Balance),
(5, 10_000 as Balance),
(6, 100_000 as Balance),
];
// All the UDs out of the reeval window must produce 0 money units
assert_eq!(
compute_claim_uds(7, 1, past_reevals.into_iter()),
(3, 111_000)
);
}
} }
...@@ -14,98 +14,129 @@ ...@@ -14,98 +14,129 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! # Duniter Universal Dividend Pallet
//!
//! One of Duniter's core features is the Universal Dividend (UD), which operates based on the Relative Theory of Money. The UD serves both as a daily monetary creation mechanism and a unit of measure within the Duniter ecosystem.
//!
//! ## Overview
//!
//! This pallet enables:
//! - Creation of Universal Dividends (UD) as a daily monetary issuance and measure unit.
//! - Transfer of currency denominated in UD between accounts.
//!
//! **Note**: The UD is not automatically created daily for every account due to resource constraints. Instead, members must claim their UD using a specific extrinsic.
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking; mod benchmarking;
mod compute_claim_uds; mod compute_claim_uds;
mod runtime_api;
mod types;
mod weights;
#[cfg(test)] #[cfg(test)]
mod mock; mod mock;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
mod types;
mod weights; #[cfg(feature = "runtime-benchmarks")]
use duniter_primitives::Idty;
pub use pallet::*; pub use pallet::*;
pub use runtime_api::*;
pub use types::*; pub use types::*;
pub use weights::WeightInfo; pub use weights::WeightInfo;
use frame_support::traits::{tokens::ExistenceRequirement, Currency}; use frame_support::traits::{
fungible::{self, Balanced, Inspect, Mutate},
tokens::{Fortitude, Precision, Preservation},
OnTimestampSet, ReservableCurrency,
};
use sp_arithmetic::{ use sp_arithmetic::{
per_things::Perbill, per_things::Perbill,
traits::{One, Saturating, Zero}, traits::{EnsureMul, One, Saturating, Zero},
}; };
use sp_runtime::traits::StaticLookup; use sp_runtime::traits::{Get, MaybeSerializeDeserialize, StaticLookup};
#[allow(unreachable_patterns)]
#[frame_support::pallet] #[frame_support::pallet]
pub mod pallet { pub mod pallet {
use super::*; use super::*;
use frame_support::pallet_prelude::*; use frame_support::{
use frame_support::traits::{StorageVersion, StoredMap}; pallet_prelude::*,
traits::{StorageVersion, StoredMap},
};
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
use sp_runtime::traits::Convert; use sp_runtime::traits::Convert;
use sp_std::vec::Vec;
pub type BalanceOf<T> = type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; pub type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
/// The current storage version. /// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet] #[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::storage_version(STORAGE_VERSION)] #[pallet::storage_version(STORAGE_VERSION)]
//#[pallet::without_storage_info]
pub struct Pallet<T>(_); pub struct Pallet<T>(_);
#[pallet::config] #[pallet::config]
pub trait Config: frame_system::Config { pub trait Config: frame_system::Config + pallet_timestamp::Config {
// BlockNumber into Balance converter /// Something that convert a Moment inot a Balance.
type BlockNumberIntoBalance: Convert<Self::BlockNumber, BalanceOf<Self>>; type MomentIntoBalance: Convert<Self::Moment, BalanceOf<Self>>;
// The currency
type Currency: Currency<Self::AccountId>; /// The currency type used in this pallet.
#[pallet::constant] type Currency: fungible::Balanced<Self::AccountId>
+ fungible::Mutate<Self::AccountId>
+ fungible::Inspect<Self::AccountId>
+ ReservableCurrency<Self::AccountId>;
/// Maximum number of past UD revaluations to keep in storage. /// Maximum number of past UD revaluations to keep in storage.
#[pallet::constant]
type MaxPastReeval: Get<u32>; type MaxPastReeval: Get<u32>;
/// Somethings that must provide the number of accounts allowed to create the universal dividend
/// Provides the number of accounts allowed to create the universal dividend.
type MembersCount: Get<BalanceOf<Self>>; type MembersCount: Get<BalanceOf<Self>>;
/// Somethings that must provide the list of accounts ids allowed to create the universal dividend
/// Storage for mapping AccountId to their first eligible UD creation time.
type MembersStorage: frame_support::traits::StoredMap<Self::AccountId, FirstEligibleUd>; type MembersStorage: frame_support::traits::StoredMap<Self::AccountId, FirstEligibleUd>;
/// An iterator over all members
type MembersStorageIter: From<Option<Vec<u8>>> /// The overarching event type for this pallet.
+ Iterator<Item = (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>; type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Square of the money growth rate per UD reevaluation period.
#[pallet::constant] #[pallet::constant]
/// Square of the money growth rate per ud reevaluation period
type SquareMoneyGrowthRate: Get<Perbill>; type SquareMoneyGrowthRate: Get<Perbill>;
/// Universal dividend creation period in milliseconds.
#[pallet::constant] #[pallet::constant]
/// Universal dividend creation period type UdCreationPeriod: Get<Self::Moment>;
type UdCreationPeriod: Get<Self::BlockNumber>;
#[pallet::constant] /// Universal dividend reevaluation period in milliseconds.
/// Universal dividend reevaluation period (in number of blocks)
type UdReevalPeriod: Get<Self::BlockNumber>;
#[pallet::constant] #[pallet::constant]
/// The number of units to divide the amounts expressed in number of UDs type UdReevalPeriod: Get<Self::Moment>;
/// Example: If you wish to express the UD amounts with a maximum precision of the order
/// of the milliUD, choose 1000 /// Type representing the weight of this pallet.
type UnitsPerUd: Get<BalanceOf<Self>>;
/// Pallet weights info
type WeightInfo: WeightInfo; type WeightInfo: WeightInfo;
/// Something that gives the IdtyIndex of an AccountId and reverse, used for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type IdtyAttr: duniter_primitives::Idty<u32, Self::AccountId>;
} }
// STORAGE // // STORAGE //
/// Current UD amount /// The current Universal Dividend value.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn current_ud)] #[pallet::getter(fn current_ud)]
pub type CurrentUd<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>; pub type CurrentUd<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
/// The default index for the current Universal Dividend.
#[pallet::type_value] #[pallet::type_value]
pub fn DefaultForCurrentUdIndex() -> UdIndex { pub fn DefaultForCurrentUdIndex() -> UdIndex {
1 1
} }
/// Current UD index /// The current Universal Dividend index.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn ud_index)] #[pallet::getter(fn ud_index)]
pub type CurrentUdIndex<T: Config> = pub type CurrentUdIndex<T: Config> =
...@@ -113,6 +144,9 @@ pub mod pallet { ...@@ -113,6 +144,9 @@ pub mod pallet {
#[cfg(test)] #[cfg(test)]
#[pallet::storage] #[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< pub type TestMembers<T: Config> = StorageMap<
_, _,
Blake2_128Concat, Blake2_128Concat,
...@@ -123,17 +157,22 @@ pub mod pallet { ...@@ -123,17 +157,22 @@ pub mod pallet {
ConstU32<300_000>, ConstU32<300_000>,
>; >;
/// Total quantity of money created by universal dividend (does not take into account the possible destruction of money) /// The total quantity of money created by Universal Dividend, excluding potential money destruction.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn total_money_created)] #[pallet::getter(fn total_money_created)]
pub type MonetaryMass<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>; pub type MonetaryMass<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
/// Next UD reevaluation /// The next Universal Dividend re-evaluation.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn next_reeval)] #[pallet::getter(fn next_reeval)]
pub type NextReeval<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>; pub type NextReeval<T: Config> = StorageValue<_, T::Moment, OptionQuery>;
/// The next Universal Dividend creation.
#[pallet::storage]
#[pallet::getter(fn next_ud)]
pub type NextUd<T: Config> = StorageValue<_, T::Moment, OptionQuery>;
/// Past UD reevaluations /// The past Universal Dividend re-evaluations.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn past_reevals)] #[pallet::getter(fn past_reevals)]
pub type PastReevals<T: Config> = pub type PastReevals<T: Config> =
...@@ -142,39 +181,60 @@ pub mod pallet { ...@@ -142,39 +181,60 @@ pub mod pallet {
// GENESIS // GENESIS
#[pallet::genesis_config] #[pallet::genesis_config]
pub struct GenesisConfig<T: Config> { pub struct GenesisConfig<T: Config>
pub first_reeval: T::BlockNumber, where
pub first_ud: BalanceOf<T>, <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)
pub initial_monetary_mass: BalanceOf<T>, pub initial_monetary_mass: BalanceOf<T>,
/// accounts of initial members
// (only for test purpose)
#[cfg(test)] #[cfg(test)]
pub initial_members: Vec<T::AccountId>, pub initial_members: Vec<T::AccountId>,
/// value of the first UD
/// expressed in amount of currency
pub ud: BalanceOf<T>,
} }
#[cfg(feature = "std")] impl<T: Config> Default for GenesisConfig<T>
impl<T: Config> Default for GenesisConfig<T> { where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
fn default() -> Self { fn default() -> Self {
Self { Self {
first_reeval: Default::default(), first_reeval: None,
first_ud: Default::default(), first_ud: None,
initial_monetary_mass: Default::default(), initial_monetary_mass: Default::default(),
#[cfg(test)] #[cfg(test)]
initial_members: Default::default(), initial_members: Default::default(),
ud: BalanceOf::<T>::one(),
} }
} }
} }
#[pallet::genesis_build] #[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { impl<T: Config> BuildGenesisConfig for GenesisConfig<T>
where
<T as pallet_timestamp::Config>::Moment: MaybeSerializeDeserialize,
{
fn build(&self) { fn build(&self) {
assert!(!self.first_ud.is_zero()); assert!(!self.ud.is_zero());
assert!(self.initial_monetary_mass >= T::Currency::total_issuance());
<CurrentUd<T>>::put(self.first_ud); <CurrentUd<T>>::put(self.ud);
// totalissuance should be updated to the same amount
<MonetaryMass<T>>::put(self.initial_monetary_mass); <MonetaryMass<T>>::put(self.initial_monetary_mass);
NextReeval::<T>::put(self.first_reeval);
NextReeval::<T>::set(self.first_reeval);
NextUd::<T>::set(self.first_ud);
let mut past_reevals = BoundedVec::default(); let mut past_reevals = BoundedVec::default();
past_reevals past_reevals
.try_push((1, self.first_ud)) .try_push((1, self.ud))
.expect("MaxPastReeval should be greather than zero"); .expect("MaxPastReeval should be greather than zero");
PastReevals::<T>::put(past_reevals); PastReevals::<T>::put(past_reevals);
...@@ -187,29 +247,6 @@ pub mod pallet { ...@@ -187,29 +247,6 @@ pub mod pallet {
} }
} }
// HOOKS //
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: T::BlockNumber) -> Weight {
if (n % T::UdCreationPeriod::get()).is_zero() {
let current_members_count = T::MembersCount::get();
let next_reeval = NextReeval::<T>::get();
if n >= next_reeval {
NextReeval::<T>::put(next_reeval.saturating_add(T::UdReevalPeriod::get()));
Self::reeval_ud(current_members_count);
Self::create_ud(current_members_count);
T::WeightInfo::on_initialize_ud_reevalued()
} else {
Self::create_ud(current_members_count);
T::WeightInfo::on_initialize_ud_created()
}
} else {
T::WeightInfo::on_initialize()
}
}
}
// EVENTS // // EVENTS //
// Pallets use events to inform users when important changes are made. // Pallets use events to inform users when important changes are made.
...@@ -231,7 +268,7 @@ pub mod pallet { ...@@ -231,7 +268,7 @@ pub mod pallet {
members_count: BalanceOf<T>, members_count: BalanceOf<T>,
}, },
/// DUs were automatically transferred as part of a member removal. /// DUs were automatically transferred as part of a member removal.
UdsAutoPaidAtRemoval { UdsAutoPaid {
count: UdIndex, count: UdIndex,
total: BalanceOf<T>, total: BalanceOf<T>,
who: T::AccountId, who: T::AccountId,
...@@ -254,7 +291,9 @@ pub mod pallet { ...@@ -254,7 +291,9 @@ pub mod pallet {
// INTERNAL FUNCTIONS // // INTERNAL FUNCTIONS //
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
fn create_ud(members_count: BalanceOf<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 ud_amount = <CurrentUd<T>>::get();
let monetary_mass = <MonetaryMass<T>>::get(); let monetary_mass = <MonetaryMass<T>>::get();
...@@ -263,9 +302,14 @@ pub mod pallet { ...@@ -263,9 +302,14 @@ pub mod pallet {
core::mem::replace(next_ud_index, next_ud_index.saturating_add(1)) core::mem::replace(next_ud_index, next_ud_index.saturating_add(1))
}); });
// compute the new monetary mass
let new_monetary_mass = let new_monetary_mass =
monetary_mass.saturating_add(ud_amount.saturating_mul(members_count)); monetary_mass.saturating_add(ud_amount.saturating_mul(members_count));
// update the storage value of the monetary mass
MonetaryMass::<T>::put(new_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 { Self::deposit_event(Event::NewUdCreated {
amount: ud_amount, amount: ud_amount,
index: ud_index, index: ud_index,
...@@ -273,6 +317,8 @@ pub mod pallet { ...@@ -273,6 +317,8 @@ pub mod pallet {
monetary_mass: new_monetary_mass, monetary_mass: new_monetary_mass,
}); });
} }
/// claim all due universal dividend at a time
fn do_claim_uds(who: &T::AccountId) -> DispatchResultWithPostInfo { fn do_claim_uds(who: &T::AccountId) -> DispatchResultWithPostInfo {
T::MembersStorage::try_mutate_exists(who, |maybe_first_eligible_ud| { T::MembersStorage::try_mutate_exists(who, |maybe_first_eligible_ud| {
if let Some(FirstEligibleUd(Some(ref mut first_ud_index))) = maybe_first_eligible_ud if let Some(FirstEligibleUd(Some(ref mut first_ud_index))) = maybe_first_eligible_ud
...@@ -289,12 +335,13 @@ pub mod pallet { ...@@ -289,12 +335,13 @@ pub mod pallet {
let _ = core::mem::replace( let _ = core::mem::replace(
first_ud_index, first_ud_index,
core::num::NonZeroU16::new(current_ud_index) core::num::NonZeroU16::new(current_ud_index)
.expect("unrechable because current_ud_index is never zero."), .expect("unreachable because current_ud_index is never zero."),
); );
T::Currency::deposit_creating(who, uds_total); // Currency is issued here
let actual_total = T::Currency::mint_into(who, uds_total)?;
Self::deposit_event(Event::UdsClaimed { Self::deposit_event(Event::UdsClaimed {
count: uds_count, count: uds_count,
total: uds_total, total: actual_total,
who: who.clone(), who: who.clone(),
}); });
Ok(().into()) Ok(().into())
...@@ -304,11 +351,13 @@ pub mod pallet { ...@@ -304,11 +351,13 @@ pub mod pallet {
} }
}) })
} }
/// like balance.transfer, but give an amount in milliUD
fn do_transfer_ud( fn do_transfer_ud(
origin: OriginFor<T>, origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source, dest: <T::Lookup as StaticLookup>::Source,
value: BalanceOf<T>, value: BalanceOf<T>,
existence_requirement: ExistenceRequirement, preservation: Preservation,
) -> DispatchResultWithPostInfo { ) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?; let who = ensure_signed(origin)?;
let dest = T::Lookup::lookup(dest)?; let dest = T::Lookup::lookup(dest)?;
...@@ -316,26 +365,30 @@ pub mod pallet { ...@@ -316,26 +365,30 @@ pub mod pallet {
T::Currency::transfer( T::Currency::transfer(
&who, &who,
&dest, &dest,
value.saturating_mul(ud_amount) / T::UnitsPerUd::get(), value.ensure_mul(ud_amount)? / 1_000u32.into(),
existence_requirement, preservation,
)?; )?;
Ok(().into()) Ok(().into())
} }
fn reeval_ud(members_count: BalanceOf<T>) {
let ud_amount = <CurrentUd<T>>::get();
/// 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(); let monetary_mass = <MonetaryMass<T>>::get();
// compute new value
let new_ud_amount = Self::reeval_ud_formula( let new_ud_amount = Self::reeval_ud_formula(
ud_amount, ud_amount,
T::SquareMoneyGrowthRate::get(), T::SquareMoneyGrowthRate::get(),
monetary_mass, monetary_mass,
members_count, members_count,
T::BlockNumberIntoBalance::convert( T::MomentIntoBalance::convert(
T::UdReevalPeriod::get() / T::UdCreationPeriod::get(), T::UdReevalPeriod::get() / T::UdCreationPeriod::get(),
), ),
); );
// update the storage value and the history of past reevals
CurrentUd::<T>::put(new_ud_amount); CurrentUd::<T>::put(new_ud_amount);
PastReevals::<T>::mutate(|past_reevals| { PastReevals::<T>::mutate(|past_reevals| {
if past_reevals.len() == T::MaxPastReeval::get() as usize { if past_reevals.len() == T::MaxPastReeval::get() as usize {
...@@ -352,6 +405,8 @@ pub mod pallet { ...@@ -352,6 +405,8 @@ pub mod pallet {
members_count, members_count,
}); });
} }
/// formula for Universal Dividend reevaluation
fn reeval_ud_formula( fn reeval_ud_formula(
ud_t: BalanceOf<T>, ud_t: BalanceOf<T>,
c_square: Perbill, c_square: Perbill,
...@@ -373,39 +428,46 @@ pub mod pallet { ...@@ -373,39 +428,46 @@ pub mod pallet {
#[pallet::call] #[pallet::call]
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// Claim Universal Dividends /// Claim Universal Dividends.
#[pallet::weight(T::WeightInfo::claim_uds(T::MaxPastReeval::get()))] #[pallet::call_index(0)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::claim_uds(T::MaxPastReeval::get()))]
pub fn claim_uds(origin: OriginFor<T>) -> DispatchResultWithPostInfo { pub fn claim_uds(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?; let who = ensure_signed(origin)?;
Self::do_claim_uds(&who) Self::do_claim_uds(&who)
} }
/// Transfer some liquid free balance to another account, in milliUD. /// Transfer some liquid free balance to another account, in milliUD.
#[pallet::weight(T::WeightInfo::transfer_ud())] #[pallet::call_index(1)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::transfer_ud())]
pub fn transfer_ud( pub fn transfer_ud(
origin: OriginFor<T>, origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source, dest: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] value: BalanceOf<T>, #[pallet::compact] value: BalanceOf<T>,
) -> DispatchResultWithPostInfo { ) -> DispatchResultWithPostInfo {
Self::do_transfer_ud(origin, dest, value, ExistenceRequirement::AllowDeath) Self::do_transfer_ud(origin, dest, value, Preservation::Expendable)
} }
/// Transfer some liquid free balance to another account, in milliUD. /// Transfer some liquid free balance to another account in milliUD and keep the account alive.
#[pallet::weight(T::WeightInfo::transfer_ud_keep_alive())] #[pallet::call_index(2)]
#[pallet::weight(<T as pallet::Config>::WeightInfo::transfer_ud_keep_alive())]
pub fn transfer_ud_keep_alive( pub fn transfer_ud_keep_alive(
origin: OriginFor<T>, origin: OriginFor<T>,
dest: <T::Lookup as StaticLookup>::Source, dest: <T::Lookup as StaticLookup>::Source,
#[pallet::compact] value: BalanceOf<T>, #[pallet::compact] value: BalanceOf<T>,
) -> DispatchResultWithPostInfo { ) -> DispatchResultWithPostInfo {
Self::do_transfer_ud(origin, dest, value, ExistenceRequirement::KeepAlive) Self::do_transfer_ud(origin, dest, value, Preservation::Preserve)
} }
} }
// PUBLIC FUNCTIONS // PUBLIC FUNCTIONS
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// Initialize the first eligible Universal Dividend index.
pub fn init_first_eligible_ud() -> FirstEligibleUd { pub fn init_first_eligible_ud() -> FirstEligibleUd {
CurrentUdIndex::<T>::get().into() CurrentUdIndex::<T>::get().into()
} }
/// Handle the removal of a member, which automatically claims Universal Dividends.
pub fn on_removed_member(first_ud_index: UdIndex, who: &T::AccountId) -> Weight { pub fn on_removed_member(first_ud_index: UdIndex, who: &T::AccountId) -> Weight {
let current_ud_index = CurrentUdIndex::<T>::get(); let current_ud_index = CurrentUdIndex::<T>::get();
if first_ud_index < current_ud_index { if first_ud_index < current_ud_index {
...@@ -414,16 +476,80 @@ pub mod pallet { ...@@ -414,16 +476,80 @@ pub mod pallet {
first_ud_index, first_ud_index,
PastReevals::<T>::get().into_iter(), PastReevals::<T>::get().into_iter(),
); );
T::Currency::deposit_creating(who, uds_total); let _ = T::Currency::deposit(who, uds_total, Precision::Exact);
Self::deposit_event(Event::UdsAutoPaidAtRemoval { Self::deposit_event(Event::UdsAutoPaid {
count: uds_count, count: uds_count,
total: uds_total, total: uds_total,
who: who.clone(), who: who.clone(),
}); });
T::DbWeight::get().reads_writes(2, 1) <T as pallet::Config>::WeightInfo::on_removed_member(first_ud_index as u32)
} else { } else {
T::DbWeight::get().reads(1) <T as pallet::Config>::WeightInfo::on_removed_member(0)
}
} }
/// Get the total balance information for an account
///
/// Returns an object with three fields:
/// - `transferable`: sum of free + unclaim_uds
/// - `reserved`: reserved balance
/// - `unclaim_uds`: amount of unclaimed UDs computed by compute_claim_uds
pub fn account_balances(who: &T::AccountId) -> crate::AccountBalances<BalanceOf<T>> {
let total_balance = T::Currency::total_balance(who);
let reducible_balance =
T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite);
// Calculate unclaimed UDs
let current_ud_index = CurrentUdIndex::<T>::get();
let maybe_first_eligible_ud = T::MembersStorage::get(who);
let unclaim_uds =
if let FirstEligibleUd(Some(ref first_ud_index)) = maybe_first_eligible_ud {
let past_reevals = PastReevals::<T>::get();
compute_claim_uds::compute_claim_uds(
current_ud_index,
first_ud_index.get(),
past_reevals.into_iter(),
)
.1
} else {
Zero::zero()
};
crate::AccountBalances {
total: total_balance.saturating_add(unclaim_uds),
transferable: reducible_balance.saturating_add(unclaim_uds),
unclaim_uds,
}
}
}
}
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()));
} }
} }
} }
...@@ -17,32 +17,29 @@ ...@@ -17,32 +17,29 @@
use super::*; use super::*;
use crate::{self as pallet_universal_dividend}; use crate::{self as pallet_universal_dividend};
use frame_support::{ use frame_support::{
parameter_types, derive_impl, parameter_types,
traits::{Everything, OnFinalize, OnInitialize}, traits::{Everything, OnFinalize, OnInitialize},
}; };
use frame_system as system; use frame_system as system;
use sp_core::H256; use sp_core::{ConstU32, H256};
use sp_runtime::{ use sp_runtime::{
testing::Header,
traits::{BlakeTwo256, IdentityLookup}, traits::{BlakeTwo256, IdentityLookup},
BuildStorage, BuildStorage,
}; };
pub const BLOCK_TIME: u64 = 6_000;
type Balance = u64; type Balance = u64;
type BlockNumber = u64;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>; type Block = frame_system::mocking::MockBlock<Test>;
// Configure a mock runtime to test the pallet. // Configure a mock runtime to test the pallet.
frame_support::construct_runtime!( frame_support::construct_runtime!(
pub enum Test where pub enum Test
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{ {
System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, System: frame_system,
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, Timestamp: pallet_timestamp,
UniversalDividend: pallet_universal_dividend::{Pallet, Storage, Config<T>, Event<T>}, Balances: pallet_balances,
UniversalDividend: pallet_universal_dividend,
} }
); );
...@@ -51,31 +48,34 @@ parameter_types! { ...@@ -51,31 +48,34 @@ parameter_types! {
pub const SS58Prefix: u8 = 42; pub const SS58Prefix: u8 = 42;
} }
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl system::Config for Test { impl system::Config for Test {
type AccountData = pallet_balances::AccountData<Balance>;
type AccountId = u32;
type BaseCallFilter = Everything; type BaseCallFilter = Everything;
type BlockWeights = (); type Block = Block;
type BlockLength = (); type BlockHashCount = BlockHashCount;
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Index = u64;
type BlockNumber = u64;
type Hash = H256; type Hash = H256;
type Hashing = BlakeTwo256; type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>; type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header; type MaxConsumers = frame_support::traits::ConstU32<16>;
type RuntimeEvent = RuntimeEvent; type Nonce = u64;
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = PalletInfo; type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<Balance>; type RuntimeCall = RuntimeCall;
type OnNewAccount = (); type RuntimeEvent = RuntimeEvent;
type OnKilledAccount = (); type RuntimeOrigin = RuntimeOrigin;
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix; type SS58Prefix = SS58Prefix;
type OnSetCode = (); }
type MaxConsumers = frame_support::traits::ConstU32<16>;
parameter_types! {
pub const MinimumPeriod: u64 = 3_000;
}
impl pallet_timestamp::Config for Test {
type MinimumPeriod = MinimumPeriod;
type Moment = u64;
type OnTimestampSet = UniversalDividend;
type WeightInfo = ();
} }
parameter_types! { parameter_types! {
...@@ -84,31 +84,37 @@ parameter_types! { ...@@ -84,31 +84,37 @@ parameter_types! {
} }
impl pallet_balances::Config for Test { impl pallet_balances::Config for Test {
type AccountStore = System;
type Balance = Balance; type Balance = Balance;
type DoneSlashHandler = ();
type DustRemoval = (); type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit; type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System; type FreezeIdentifier = ();
type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>; type MaxFreezes = ConstU32<0>;
type MaxLocks = MaxLocks; type MaxLocks = MaxLocks;
type MaxReserves = (); type MaxReserves = ();
type ReserveIdentifier = [u8; 8]; type ReserveIdentifier = [u8; 8];
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type RuntimeFreezeReason = ();
type RuntimeHoldReason = ();
type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>;
} }
parameter_types! { parameter_types! {
pub const MembersCount: u64 = 3; pub const MembersCount: u64 = 3;
pub const SquareMoneyGrowthRate: Perbill = Perbill::from_percent(10); pub const SquareMoneyGrowthRate: Perbill = Perbill::from_percent(10);
pub const UdCreationPeriod: BlockNumber = 2; pub const UdCreationPeriod: u64 = 12_000;
pub const UdReevalPeriod: BlockNumber = 8; pub const UdReevalPeriod: u64 = 48_000;
} }
pub struct TestMembersStorage; pub struct TestMembersStorage;
impl frame_support::traits::StoredMap<u64, FirstEligibleUd> for TestMembersStorage { impl frame_support::traits::StoredMap<u32, FirstEligibleUd> for TestMembersStorage {
fn get(key: &u64) -> FirstEligibleUd { fn get(key: &u32) -> FirstEligibleUd {
crate::TestMembers::<Test>::get(key) crate::TestMembers::<Test>::get(key)
} }
fn try_mutate_exists<R, E: From<sp_runtime::DispatchError>>( fn try_mutate_exists<R, E: From<sp_runtime::DispatchError>>(
key: &u64, key: &u32,
f: impl FnOnce(&mut Option<FirstEligibleUd>) -> Result<R, E>, f: impl FnOnce(&mut Option<FirstEligibleUd>) -> Result<R, E>,
) -> Result<R, E> { ) -> Result<R, E> {
let mut value = Some(crate::TestMembers::<Test>::get(key)); let mut value = Some(crate::TestMembers::<Test>::get(key));
...@@ -119,36 +125,19 @@ impl frame_support::traits::StoredMap<u64, FirstEligibleUd> for TestMembersStora ...@@ -119,36 +125,19 @@ impl frame_support::traits::StoredMap<u64, FirstEligibleUd> for TestMembersStora
Ok(result) Ok(result)
} }
} }
pub struct TestMembersStorageIter(frame_support::storage::PrefixIterator<(u64, FirstEligibleUd)>);
impl From<Option<Vec<u8>>> for TestMembersStorageIter {
fn from(maybe_key: Option<Vec<u8>>) -> Self {
let mut iter = crate::TestMembers::<Test>::iter();
if let Some(key) = maybe_key {
iter.set_last_raw_key(key);
}
Self(iter)
}
}
impl Iterator for TestMembersStorageIter {
type Item = (u64, FirstEligibleUd);
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl pallet_universal_dividend::Config for Test { impl pallet_universal_dividend::Config for Test {
type BlockNumberIntoBalance = sp_runtime::traits::ConvertInto;
type Currency = pallet_balances::Pallet<Test>; type Currency = pallet_balances::Pallet<Test>;
#[cfg(feature = "runtime-benchmarks")]
type IdtyAttr = ();
type MaxPastReeval = frame_support::traits::ConstU32<2>; type MaxPastReeval = frame_support::traits::ConstU32<2>;
type MembersCount = MembersCount; type MembersCount = MembersCount;
type MembersStorage = TestMembersStorage; type MembersStorage = TestMembersStorage;
type MembersStorageIter = TestMembersStorageIter; type MomentIntoBalance = sp_runtime::traits::ConvertInto;
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type SquareMoneyGrowthRate = SquareMoneyGrowthRate; type SquareMoneyGrowthRate = SquareMoneyGrowthRate;
type UdCreationPeriod = UdCreationPeriod; type UdCreationPeriod = UdCreationPeriod;
type UdReevalPeriod = UdReevalPeriod; type UdReevalPeriod = UdReevalPeriod;
type UnitsPerUd = frame_support::traits::ConstU64<1_000>;
type WeightInfo = (); type WeightInfo = ();
} }
...@@ -156,9 +145,11 @@ impl pallet_universal_dividend::Config for Test { ...@@ -156,9 +145,11 @@ impl pallet_universal_dividend::Config for Test {
pub fn new_test_ext( pub fn new_test_ext(
gen_conf: pallet_universal_dividend::GenesisConfig<Test>, gen_conf: pallet_universal_dividend::GenesisConfig<Test>,
) -> sp_io::TestExternalities { ) -> sp_io::TestExternalities {
GenesisConfig { RuntimeGenesisConfig {
system: SystemConfig::default(), system: SystemConfig::default(),
balances: BalancesConfig::default(), balances: BalancesConfig {
total_issuance: gen_conf.initial_monetary_mass,
},
universal_dividend: gen_conf, universal_dividend: gen_conf,
} }
.build_storage() .build_storage()
...@@ -174,5 +165,11 @@ pub fn run_to_block(n: u64) { ...@@ -174,5 +165,11 @@ pub fn run_to_block(n: u64) {
System::set_block_number(System::block_number() + 1); System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number()); System::on_initialize(System::block_number());
UniversalDividend::on_initialize(System::block_number()); UniversalDividend::on_initialize(System::block_number());
Timestamp::set_timestamp(System::block_number() * BLOCK_TIME);
} }
} }
/// Helper function to mint tokens for testing purposes
pub fn mint_into(who: &u32, amount: Balance) -> Result<Balance, sp_runtime::DispatchError> {
<Test as pallet_universal_dividend::Config>::Currency::mint_into(who, amount)
}
// Copyright 2021 Axiom-Team
//
// 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/>.
use codec::{Codec, Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::RuntimeDebug;
sp_api::decl_runtime_apis! {
/// Runtime API for Universal Dividend pallet
pub trait UniversalDividendApi<AccountId, Balance>
where
AccountId: Codec,
AccountBalances<Balance>: Codec,
{
/// Get the total balance information for an account
///
/// Returns an object with three fields:
/// - `total`: total balance
/// - `transferable`: sum of reducible + unclaim_uds
/// - `unclaim_uds`: amount of unclaimed UDs
fn account_balances(account: AccountId) -> AccountBalances<Balance>;
}
}
/// Account total balance information
#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, RuntimeDebug)]
pub struct AccountBalances<Balance> {
/// The total amount of funds for which the user is the ultimate beneficial owner.
/// Includes funds that may not be transferable (e.g., reserved balance, existential deposit).
pub total: Balance,
/// The maximum amount of funds that can be successfully withdrawn or transferred
/// (includes unclaimed UDs).
pub transferable: Balance,
/// The total amount of unclaimed UDs (accounts for any re-evaluations of UDs).
pub unclaim_uds: Balance,
}
...@@ -15,15 +15,17 @@ ...@@ -15,15 +15,17 @@
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use crate::mock::*; use crate::mock::*;
use frame_support::{assert_err, assert_ok, assert_storage_noop}; use frame_support::{assert_err, assert_ok, assert_storage_noop, traits::ReservableCurrency};
use sp_runtime::{ArithmeticError, DispatchError};
#[test] #[test]
fn test_claim_uds() { fn test_claim_uds() {
new_test_ext(UniversalDividendConfig { new_test_ext(UniversalDividendConfig {
first_reeval: 8, first_reeval: Some(48_000),
first_ud: 1_000, first_ud: Some(12_000),
initial_monetary_mass: 0, initial_monetary_mass: 0,
initial_members: vec![1, 2, 3], initial_members: vec![1, 2, 3],
ud: 1_000,
}) })
.execute_with(|| { .execute_with(|| {
// In the beginning there was no money // In the beginning there was no money
...@@ -61,6 +63,11 @@ fn test_claim_uds() { ...@@ -61,6 +63,11 @@ fn test_claim_uds() {
total: 1_000, total: 1_000,
who: 1, who: 1,
})); }));
// the expected event from pallet balances is Minted
System::assert_has_event(RuntimeEvent::Balances(pallet_balances::Event::Minted {
who: 1,
amount: 1000,
}));
assert_eq!(Balances::free_balance(1), 1_000); assert_eq!(Balances::free_balance(1), 1_000);
// Others members should not receive any UDs with Alice claim // Others members should not receive any UDs with Alice claim
assert_eq!(Balances::free_balance(2), 0); assert_eq!(Balances::free_balance(2), 0);
...@@ -118,16 +125,126 @@ fn test_claim_uds() { ...@@ -118,16 +125,126 @@ fn test_claim_uds() {
who: 3, who: 3,
})); }));
assert_eq!(Balances::free_balance(3), 4_075); assert_eq!(Balances::free_balance(3), 4_075);
// At block #16, the second reevaluated UD should be created
run_to_block(16);
assert_eq!(UniversalDividend::total_money_created(), 25_671);
// Charlie can claim new UD, he must receive exactly four UDs
assert_ok!(UniversalDividend::claim_uds(RuntimeOrigin::signed(3)));
System::assert_has_event(RuntimeEvent::UniversalDividend(crate::Event::UdsClaimed {
count: 4,
total: 4_482,
who: 3,
}));
assert_eq!(Balances::free_balance(3), 8557);
});
}
#[test]
fn test_claim_uds_using_genesis_timestamp() {
new_test_ext(UniversalDividendConfig {
first_reeval: None,
first_ud: None,
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// In the beginning there was no money
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::free_balance(2), 0);
assert_eq!(Balances::free_balance(3), 0);
assert_eq!(Balances::free_balance(4), 0);
assert_eq!(UniversalDividend::total_money_created(), 0);
// Alice can claim UDs, but this should be a no-op.
run_to_block(1);
assert_storage_noop!(assert_ok!(UniversalDividend::claim_uds(
RuntimeOrigin::signed(1)
)));
assert_eq!(Balances::free_balance(1), 0);
// Dave is not a member, he can't claim UDs
assert_err!(
UniversalDividend::claim_uds(RuntimeOrigin::signed(4)),
crate::Error::<Test>::AccountNotAllowedToClaimUds
);
// At block #3, the first UD must be created, but nobody should receive money
run_to_block(3);
assert_eq!(UniversalDividend::total_money_created(), 3_000);
assert_eq!(Balances::free_balance(1), 0);
assert_eq!(Balances::free_balance(2), 0);
assert_eq!(Balances::free_balance(3), 0);
assert_eq!(Balances::free_balance(4), 0);
// Alice can claim UDs, and this time she must receive exactly one UD
assert_ok!(UniversalDividend::claim_uds(RuntimeOrigin::signed(1)));
System::assert_has_event(RuntimeEvent::UniversalDividend(crate::Event::UdsClaimed {
count: 1,
total: 1_000,
who: 1,
}));
assert_eq!(Balances::free_balance(1), 1_000);
// Others members should not receive any UDs with Alice claim
assert_eq!(Balances::free_balance(2), 0);
assert_eq!(Balances::free_balance(3), 0);
assert_eq!(Balances::free_balance(4), 0);
// At block #5, the second UD must be created, but nobody should receive money
run_to_block(5);
assert_eq!(UniversalDividend::total_money_created(), 6_000);
assert_eq!(Balances::free_balance(1), 1_000);
assert_eq!(Balances::free_balance(2), 0);
assert_eq!(Balances::free_balance(3), 0);
assert_eq!(Balances::free_balance(4), 0);
// Alice can claim UDs, And she must receive exactly one UD (the second one)
assert_ok!(UniversalDividend::claim_uds(RuntimeOrigin::signed(1)));
System::assert_has_event(RuntimeEvent::UniversalDividend(crate::Event::UdsClaimed {
count: 1,
total: 1_000,
who: 1,
}));
assert_eq!(Balances::free_balance(1), 2_000);
// Others members should not receive any UDs with Alice claim
assert_eq!(Balances::free_balance(2), 0);
assert_eq!(Balances::free_balance(3), 0);
assert_eq!(Balances::free_balance(4), 0);
// Bob can claim UDs, he must receive exactly two UDs
assert_ok!(UniversalDividend::claim_uds(RuntimeOrigin::signed(2)));
System::assert_has_event(RuntimeEvent::UniversalDividend(crate::Event::UdsClaimed {
count: 2,
total: 2_000,
who: 2,
}));
assert_eq!(Balances::free_balance(2), 2_000);
// Others members should not receive any UDs with Alice and Bob claims
assert_eq!(Balances::free_balance(3), 0);
assert_eq!(Balances::free_balance(4), 0);
// Dave is still not a member, he still can't claim UDs.
assert_err!(
UniversalDividend::claim_uds(RuntimeOrigin::signed(4)),
crate::Error::<Test>::AccountNotAllowedToClaimUds
);
// At block #11, the first reevaluated UD should be created
run_to_block(11);
assert_eq!(UniversalDividend::total_money_created(), 15_300);
}); });
} }
#[test] #[test]
fn test_ud_creation() { fn test_ud_creation() {
new_test_ext(UniversalDividendConfig { new_test_ext(UniversalDividendConfig {
first_reeval: 8, first_reeval: Some(48_000),
first_ud: 1_000, first_ud: Some(12_000),
initial_monetary_mass: 0, initial_monetary_mass: 0,
initial_members: vec![1, 2, 3], initial_members: vec![1, 2, 3],
ud: 1_000,
}) })
.execute_with(|| { .execute_with(|| {
// In the beginning there was no money // In the beginning there was no money
...@@ -238,3 +355,284 @@ fn test_ud_creation() { ...@@ -238,3 +355,284 @@ fn test_ud_creation() {
assert_eq!(UniversalDividend::total_money_created(), 25_671); assert_eq!(UniversalDividend::total_money_created(), 25_671);
}); });
} }
#[test]
fn test_account_balances() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Initially, all accounts have zero balance
let balance_info = UniversalDividend::account_balances(&1);
assert_eq!(balance_info.transferable, 0);
assert_eq!(balance_info.total, 0);
assert_eq!(balance_info.unclaim_uds, 0);
// Create some UDs and claim them
run_to_block(2);
assert_ok!(UniversalDividend::claim_uds(RuntimeOrigin::signed(1)));
// Check balance after claiming
let balance_info = UniversalDividend::account_balances(&1);
assert_eq!(balance_info.transferable, 1000 - 10); // free (1000) + unclaim_uds (0) - existantial deposit (10)
assert_eq!(balance_info.total, 1000); // transferable + reserved
assert_eq!(balance_info.unclaim_uds, 0);
// Create more UDs but don't claim them
run_to_block(4);
let balance_info = UniversalDividend::account_balances(&1);
assert_eq!(balance_info.transferable, 1000 + 1000 - 10); // free (1000) + unclaim_uds (1000) - existantial deposit (10)
assert_eq!(balance_info.total, 2000); // transferable + reserved
assert_eq!(balance_info.unclaim_uds, 1000);
// Test with reserved balance
assert_ok!(Balances::reserve(&1, 500));
let balance_info = UniversalDividend::account_balances(&1);
assert_eq!(balance_info.transferable, 500 + 1000 - 10); // free (500) + unclaim_uds (1000) - existantial deposit (10)
assert_eq!(balance_info.total, 2000); // transferable + reserved
assert_eq!(balance_info.unclaim_uds, 1000);
// Test non-member account
let balance_info = UniversalDividend::account_balances(&4);
assert_eq!(balance_info.transferable, 0);
assert_eq!(balance_info.total, 0);
assert_eq!(balance_info.unclaim_uds, 0);
});
}
#[test]
fn test_transfer_ud_overflow() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 some balance to work with
let _ = mint_into(&1, 1_000_000);
assert_eq!(Balances::free_balance(1), 1_000_000);
// Test overflow scenario: try to transfer a very large value in milliUD
// that when multiplied by current_ud (1000) would overflow u64
let max_u64 = u64::MAX;
let overflow_value = max_u64 / 1000 + 1; // This will overflow when multiplied by 1000
assert_err!(
UniversalDividend::transfer_ud(RuntimeOrigin::signed(1), 2, overflow_value),
DispatchError::Arithmetic(ArithmeticError::Overflow),
);
});
}
#[test]
fn test_transfer_ud_keep_alive_overflow() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 some balance to work with
let _ = mint_into(&1, 1_000_000);
assert_eq!(Balances::free_balance(1), 1_000_000);
// Test overflow scenario: try to transfer a very large value in milliUD
// that when multiplied by current_ud (1000) would overflow u64
let max_u64 = u64::MAX;
let overflow_value = max_u64 / 1000 + 1; // This will overflow when multiplied by 1000
assert_err!(
UniversalDividend::transfer_ud_keep_alive(RuntimeOrigin::signed(1), 2, overflow_value),
DispatchError::Arithmetic(ArithmeticError::Overflow),
);
});
}
#[test]
fn test_transfer_ud_underflow() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 some balance to work with
let _ = mint_into(&1, 1_000_000);
assert_eq!(Balances::free_balance(1), 1_000_000);
// Test underflow scenario: try to transfer a value that when divided by 1000
// would result in 0 (which is not a valid transfer amount)
let underflow_value = 999; // 999 * 1000 / 1000 = 999, which is valid
// This should work because 999 milliUD = 999 actual units
assert_ok!(UniversalDividend::transfer_ud(
RuntimeOrigin::signed(1),
2,
underflow_value
));
assert_eq!(Balances::free_balance(2), 999);
// Test with minimum valid value (1000 milliUD = 1 UD)
assert_ok!(UniversalDividend::transfer_ud(
RuntimeOrigin::signed(1),
2,
1000
));
assert_eq!(Balances::free_balance(2), 1999); // 999 + 1000
});
}
#[test]
fn test_transfer_ud_keep_alive_underflow() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 some balance to work with
let _ = mint_into(&1, 1_000_000);
assert_eq!(Balances::free_balance(1), 1_000_000);
// Test underflow scenario: try to transfer a value that when divided by 1000
// would result in 0 (which is not a valid transfer amount)
let underflow_value = 999; // 999 * 1000 / 1000 = 999, which is valid
// This should work because 999 milliUD = 999 actual units
assert_ok!(UniversalDividend::transfer_ud_keep_alive(
RuntimeOrigin::signed(1),
2,
underflow_value
));
assert_eq!(Balances::free_balance(2), 999);
// Test with minimum valid value (1000 milliUD = 1 UD)
assert_ok!(UniversalDividend::transfer_ud_keep_alive(
RuntimeOrigin::signed(1),
2,
1000
));
assert_eq!(Balances::free_balance(2), 1999); // 999 + 1000
});
}
#[test]
fn test_transfer_ud_edge_cases() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 some balance to work with
let _ = mint_into(&1, 1_000_000);
assert_eq!(Balances::free_balance(1), 1_000_000);
// Test with zero value (should work - 0 * 1000 / 1000 = 0)
assert_ok!(UniversalDividend::transfer_ud(
RuntimeOrigin::signed(1),
2,
0
));
assert_eq!(Balances::free_balance(2), 0);
// Test with very small values that should work
assert_ok!(UniversalDividend::transfer_ud(
RuntimeOrigin::signed(1),
2,
1000
)); // 1 UD
assert_eq!(Balances::free_balance(2), 1000);
assert_ok!(UniversalDividend::transfer_ud(
RuntimeOrigin::signed(1),
2,
1500
)); // 1.5 UD
assert_eq!(Balances::free_balance(2), 2500);
});
}
#[test]
fn test_transfer_ud_keep_alive_edge_cases() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 some balance to work with
let _ = mint_into(&1, 1_000_000);
assert_eq!(Balances::free_balance(1), 1_000_000);
// Test with zero value (should work - 0 * 1000 / 1000 = 0)
assert_ok!(UniversalDividend::transfer_ud_keep_alive(
RuntimeOrigin::signed(1),
2,
0
));
assert_eq!(Balances::free_balance(2), 0);
// Test with very small values that should work
assert_ok!(UniversalDividend::transfer_ud_keep_alive(
RuntimeOrigin::signed(1),
2,
1000
)); // 1 UD
assert_eq!(Balances::free_balance(2), 1000);
assert_ok!(UniversalDividend::transfer_ud_keep_alive(
RuntimeOrigin::signed(1),
2,
1500
)); // 1.5 UD
assert_eq!(Balances::free_balance(2), 2500);
});
}
#[test]
fn test_transfer_ud_insufficient_balance() {
new_test_ext(UniversalDividendConfig {
first_reeval: Some(48_000),
first_ud: Some(12_000),
initial_monetary_mass: 0,
initial_members: vec![1, 2, 3],
ud: 1_000,
})
.execute_with(|| {
// Give account 1 minimal balance
let _ = mint_into(&1, 100);
assert_eq!(Balances::free_balance(1), 100);
// Try to transfer more than available balance
assert_err!(
UniversalDividend::transfer_ud(RuntimeOrigin::signed(1), 2, 2000), // Would require 2000 balance
DispatchError::Arithmetic(ArithmeticError::Underflow),
);
// Try to transfer exactly the available balance
assert_ok!(UniversalDividend::transfer_ud(
RuntimeOrigin::signed(1),
2,
100
)); // 100 milliUD = 0.1 UD
assert_eq!(Balances::free_balance(2), 100);
assert_eq!(Balances::free_balance(1), 0);
});
}
...@@ -14,21 +14,28 @@ ...@@ -14,21 +14,28 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use codec::{Decode, Encode, Error, Input, MaxEncodedLen, Output}; use codec::{Decode, DecodeWithMemTracking, Encode, Error, Input, MaxEncodedLen, Output};
use core::num::NonZeroU16; use core::num::NonZeroU16;
#[cfg(feature = "std")] use scale_info::prelude::vec::Vec;
use serde::{Deserialize, Serialize};
use sp_runtime::RuntimeDebug; use sp_runtime::RuntimeDebug;
use sp_std::vec::Vec;
pub type UdIndex = u16; pub type UdIndex = u16;
#[cfg_attr(feature = "std", derive(Deserialize, Serialize))] /// Represents the first eligible Universal Dividend.
#[derive(Clone, Copy, Default, Eq, PartialEq, RuntimeDebug)] #[derive(
Clone, Eq, DecodeWithMemTracking, PartialEq, RuntimeDebug, serde::Deserialize, serde::Serialize,
)]
pub struct FirstEligibleUd(pub Option<NonZeroU16>); pub struct FirstEligibleUd(pub Option<NonZeroU16>);
#[cfg(feature = "std")] /// Default is not eligible
impl Default for FirstEligibleUd {
fn default() -> Self {
FirstEligibleUd(None)
}
}
impl FirstEligibleUd { impl FirstEligibleUd {
/// Eligible at the first UD index
pub fn min() -> Self { pub fn min() -> Self {
Self(Some(NonZeroU16::new(1).expect("unreachable"))) Self(Some(NonZeroU16::new(1).expect("unreachable")))
} }
......
...@@ -20,66 +20,43 @@ use frame_support::weights::{constants::RocksDbWeight, Weight}; ...@@ -20,66 +20,43 @@ use frame_support::weights::{constants::RocksDbWeight, Weight};
/// Weight functions needed for pallet_universal_dividend. /// Weight functions needed for pallet_universal_dividend.
pub trait WeightInfo { pub trait WeightInfo {
fn on_initialize() -> Weight; fn claim_uds(i: u32) -> Weight;
fn on_initialize_ud_created() -> Weight;
fn on_initialize_ud_reevalued() -> Weight;
fn claim_uds(n: u32) -> Weight;
fn transfer_ud() -> Weight; fn transfer_ud() -> Weight;
fn transfer_ud_keep_alive() -> Weight; fn transfer_ud_keep_alive() -> Weight;
fn on_removed_member(i: u32) -> Weight;
} }
// Insecure weights implementation, use it for tests only! // Insecure weights implementation, use it for tests only!
impl WeightInfo for () { impl WeightInfo for () {
// Storage: (r:0 w:0) fn claim_uds(i: u32) -> Weight {
fn on_initialize() -> Weight { Weight::from_parts(32_514_000, 0)
Weight::from_ref_time(2_260_000)
}
// Storage: Membership CounterForMembership (r:1 w:0)
// Storage: UniversalDividend NextReeval (r:1 w:0)
// Storage: UniversalDividend CurrentUd (r:1 w:0)
// Storage: UniversalDividend MonetaryMass (r:1 w:1)
// Storage: UniversalDividend CurrentUdIndex (r:1 w:1)
fn on_initialize_ud_created() -> Weight {
Weight::from_ref_time(20_160_000)
.saturating_add(RocksDbWeight::get().reads(5))
.saturating_add(RocksDbWeight::get().writes(2))
}
// Storage: Membership CounterForMembership (r:1 w:0)
// Storage: UniversalDividend NextReeval (r:1 w:1)
// Storage: UniversalDividend CurrentUd (r:1 w:1)
// Storage: UniversalDividend MonetaryMass (r:1 w:1)
// Storage: UniversalDividend PastReevals (r:1 w:1)
// Storage: UniversalDividend CurrentUdIndex (r:1 w:1)
fn on_initialize_ud_reevalued() -> Weight {
Weight::from_ref_time(32_770_000)
.saturating_add(RocksDbWeight::get().reads(6))
.saturating_add(RocksDbWeight::get().writes(5))
}
// Storage: Identity IdentityIndexOf (r:1 w:0)
// Storage: Identity Identities (r:1 w:1)
// Storage: UniversalDividend CurrentUdIndex (r:1 w:0)
// Storage: UniversalDividend PastReevals (r:1 w:0)
fn claim_uds(n: u32) -> Weight {
Weight::from_ref_time(32_514_000)
// Standard Error: 32_000 // Standard Error: 32_000
.saturating_add(Weight::from_ref_time(8_000).saturating_mul(n as u64)) .saturating_add(Weight::from_parts(8_000, 0).saturating_mul(i as u64))
.saturating_add(RocksDbWeight::get().reads(4)) .saturating_add(RocksDbWeight::get().reads(4))
.saturating_add(RocksDbWeight::get().writes(1)) .saturating_add(RocksDbWeight::get().writes(1))
} }
// Storage: UniversalDividend CurrentUd (r:1 w:0) // Storage: UniversalDividend CurrentUd (r:1 w:0)
// Storage: System Account (r:1 w:1) // Storage: System Account (r:1 w:1)
// Storage: Account PendingNewAccounts (r:0 w:1)
fn transfer_ud() -> Weight { fn transfer_ud() -> Weight {
Weight::from_ref_time(53_401_000) Weight::from_parts(53_401_000, 0)
.saturating_add(RocksDbWeight::get().reads(2)) .saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(2)) .saturating_add(RocksDbWeight::get().writes(2))
} }
// Storage: UniversalDividend CurrentUd (r:1 w:0) // Storage: UniversalDividend CurrentUd (r:1 w:0)
// Storage: System Account (r:1 w:1) // Storage: System Account (r:1 w:1)
// Storage: Account PendingNewAccounts (r:0 w:1)
fn transfer_ud_keep_alive() -> Weight { fn transfer_ud_keep_alive() -> Weight {
Weight::from_ref_time(33_420_000) Weight::from_parts(33_420_000, 0)
.saturating_add(RocksDbWeight::get().reads(2)) .saturating_add(RocksDbWeight::get().reads(2))
.saturating_add(RocksDbWeight::get().writes(2)) .saturating_add(RocksDbWeight::get().writes(2))
} }
fn on_removed_member(i: u32) -> Weight {
Weight::from_parts(32_514_000, 0)
// Standard Error: 32_000
.saturating_add(Weight::from_parts(8_000, 0).saturating_mul(i as u64))
.saturating_add(RocksDbWeight::get().reads(4))
.saturating_add(RocksDbWeight::get().writes(1))
}
} }
[package] [package]
authors = ['librelois <c@elo.tf>'] authors.workspace = true
description = 'FRAME pallet to upgrade specified origin to root.' description = "duniter pallet to upgrade specified origin to root"
edition = "2021" edition.workspace = true
homepage = 'https://duniter.org' homepage.workspace = true
license = 'AGPL-3.0' license.workspace = true
name = 'pallet-upgrade-origin' name = "pallet-upgrade-origin"
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' repository.workspace = true
version = '3.0.0' version.workspace = true
[features] [features]
default = ['std'] default = ["std"]
runtime-benchmarks = ['frame-benchmarking/runtime-benchmarks'] runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-staking/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
"frame-system/try-runtime",
"sp-runtime/try-runtime",
]
std = [ std = [
'codec/std', "codec/std",
'frame-support/std', "frame-benchmarking?/std",
'frame-system/std', "frame-support/std",
'frame-benchmarking/std', "frame-system/std",
"scale-info/std",
"sp-core/std",
"sp-io/std", "sp-io/std",
"sp-std/std", "sp-runtime/std",
"sp-staking/std",
] ]
try-runtime = ['frame-support/try-runtime']
[dependencies]
# substrate
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
[dependencies.codec]
default-features = false
features = ['derive']
package = 'parity-scale-codec'
version = "3.1.5"
[dependencies.frame-benchmarking]
default-features = false
git = 'https://github.com/duniter/substrate'
optional = true
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-support]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.frame-system] [package.metadata.docs.rs]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-io]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-std]
default-features = false
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
default-features = false default-features = false
git = 'https://github.com/duniter/substrate' targets = ["x86_64-unknown-linux-gnu"]
branch = 'duniter-substrate-v0.9.32'
### DOC ###
[package.metadata.docs.rs] [dependencies]
targets = ['x86_64-unknown-linux-gnu'] codec = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
scale-info = { workspace = true, features = ["derive"] }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-runtime = { workspace = true }
sp-staking = { workspace = true }
# Duniter upgrade origin pallet
TODO
\ No newline at end of file
...@@ -19,12 +19,20 @@ ...@@ -19,12 +19,20 @@
use super::*; use super::*;
use crate::Pallet; use crate::Pallet;
use frame_benchmarking::benchmarks; use frame_benchmarking::v2::*;
use frame_support::traits::Get; use frame_support::traits::Get;
use scale_info::prelude::vec;
benchmarks! { #[benchmarks]
dispatch_as_root { mod benchmarks {
use super::*;
#[benchmark]
fn dispatch_as_root() {
let call = Box::new(frame_system::Call::remark { remark: vec![] }.into()); let call = Box::new(frame_system::Call::remark { remark: vec![] }.into());
let origin: T::WorstCaseOriginType = T::WorstCaseOrigin::get(); let origin: T::WorstCaseOriginType = T::WorstCaseOrigin::get();
}: dispatch_as_root (origin, call)
#[extrinsic_call]
_(origin, call);
}
} }