From 8892156e827a166b0491697983408923ac35285a Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Sat, 15 Jan 2022 22:05:58 +0100
Subject: [PATCH] add pallet membership

---
 Cargo.lock                             |  17 +
 Cargo.toml                             |   1 +
 integration-tests/Cargo.toml           |   2 +-
 node/Cargo.toml                        |   2 +-
 pallets/certification/Cargo.toml       |   2 +-
 pallets/certification/src/lib.rs       |   2 +
 pallets/identity/Cargo.toml            |   2 +-
 pallets/membership/Cargo.toml          |  89 +++++
 pallets/membership/src/lib.rs          | 509 +++++++++++++++++++++++++
 pallets/membership/src/mock.rs         | 139 +++++++
 pallets/membership/src/tests.rs        | 199 ++++++++++
 pallets/membership/src/traits.rs       | 119 ++++++
 pallets/membership/src/types.rs        |  58 +++
 pallets/ud-accounts-storage/Cargo.toml |   2 +-
 pallets/universal-dividend/Cargo.toml  |   2 +-
 runtime/g1/Cargo.toml                  |   2 +-
 runtime/gdev/Cargo.toml                |   2 +-
 runtime/gtest/Cargo.toml               |   2 +-
 18 files changed, 1142 insertions(+), 9 deletions(-)
 create mode 100644 pallets/membership/Cargo.toml
 create mode 100644 pallets/membership/src/lib.rs
 create mode 100644 pallets/membership/src/mock.rs
 create mode 100644 pallets/membership/src/tests.rs
 create mode 100644 pallets/membership/src/traits.rs
 create mode 100644 pallets/membership/src/types.rs

diff --git a/Cargo.lock b/Cargo.lock
index bf0582be3..9c56b56a5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4572,6 +4572,23 @@ dependencies = [
  "sp-std",
 ]
 
+[[package]]
+name = "pallet-membership"
+version = "3.0.0"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "maplit",
+ "parity-scale-codec",
+ "scale-info",
+ "serde",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+]
+
 [[package]]
 name = "pallet-multisig"
 version = "4.0.0-dev"
diff --git a/Cargo.toml b/Cargo.toml
index f896880d7..2e4c71d5e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
     'node',
     'pallets/certification',
     'pallets/identity',
+	'pallets/membership',
     'pallets/ud-accounts-storage',
     'pallets/universal-dividend',
 ]
diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml
index c4971d2c6..9bcb1a675 100644
--- a/integration-tests/Cargo.toml
+++ b/integration-tests/Cargo.toml
@@ -5,7 +5,7 @@ edition = '2018'
 homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'duniter-integration-tests'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 
 [dev-dependencies]
diff --git a/node/Cargo.toml b/node/Cargo.toml
index fe76594fb..313c494a3 100644
--- a/node/Cargo.toml
+++ b/node/Cargo.toml
@@ -6,7 +6,7 @@ edition = '2018'
 homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'duniter'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 
 [package.metadata.docs.rs]
diff --git a/pallets/certification/Cargo.toml b/pallets/certification/Cargo.toml
index 59964b9ba..c327cd281 100644
--- a/pallets/certification/Cargo.toml
+++ b/pallets/certification/Cargo.toml
@@ -6,7 +6,7 @@ homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'pallet-certification'
 readme = 'README.md'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 
 [features]
diff --git a/pallets/certification/src/lib.rs b/pallets/certification/src/lib.rs
index de8784028..a129d24e9 100644
--- a/pallets/certification/src/lib.rs
+++ b/pallets/certification/src/lib.rs
@@ -85,6 +85,8 @@ pub mod pallet {
         type ValidityPeriod: Get<Self::BlockNumber>;
     }
 
+    // GENESIS STUFFĂ‚ //
+
     #[pallet::genesis_config]
     pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
         pub certs_by_issuer: BTreeMap<T::IdtyIndex, BTreeMap<T::IdtyIndex, T::BlockNumber>>,
diff --git a/pallets/identity/Cargo.toml b/pallets/identity/Cargo.toml
index 84202ac65..0fbcb113a 100644
--- a/pallets/identity/Cargo.toml
+++ b/pallets/identity/Cargo.toml
@@ -6,7 +6,7 @@ homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'pallet-identity'
 readme = 'README.md'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 
 [features]
diff --git a/pallets/membership/Cargo.toml b/pallets/membership/Cargo.toml
new file mode 100644
index 000000000..1b49766c4
--- /dev/null
+++ b/pallets/membership/Cargo.toml
@@ -0,0 +1,89 @@
+[package]
+authors = ['librelois <c@elo.tf>']
+description = 'FRAME pallet membership.'
+edition = '2018'
+homepage = 'https://substrate.dev'
+license = 'AGPL-3.0'
+name = 'pallet-membership'
+readme = 'README.md'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
+version = '3.0.0'
+
+[features]
+default = ['std']
+runtime-benchmarks = ['frame-benchmarking']
+std = [
+    'codec/std',
+    'frame-support/std',
+    'frame-system/std',
+    'frame-benchmarking/std',
+    'serde',
+    'sp-core/std',
+    'sp-runtime/std',
+	'sp-std/std',
+]
+try-runtime = ['frame-support/try-runtime']
+
+[dependencies]
+
+# substrate
+scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '2.3.1'
+
+[dependencies.frame-benchmarking]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+optional = true
+branch = 'duniter-monthly-2022-01'
+
+[dependencies.frame-support]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+branch = 'duniter-monthly-2022-01'
+
+[dependencies.frame-system]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+branch = 'duniter-monthly-2022-01'
+
+[dependencies.serde]
+version = "1.0.101"
+optional = true
+features = ["derive"]
+
+[dependencies.sp-core]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+branch = 'duniter-monthly-2022-01'
+
+[dependencies.sp-runtime]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+branch = 'duniter-monthly-2022-01'
+
+[dependencies.sp-std]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+branch = 'duniter-monthly-2022-01'
+
+### DOC ###
+
+[package.metadata.docs.rs]
+targets = ['x86_64-unknown-linux-gnu']
+[dev-dependencies.serde]
+version = '1.0.119'
+
+### DEV ###
+
+[dev-dependencies.maplit]
+version = '1.0.2'
+
+[dev-dependencies.sp-io]
+default-features = false
+git = 'https://github.com/librelois/substrate.git'
+branch = 'duniter-monthly-2022-01'
diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs
new file mode 100644
index 000000000..7ecfdec32
--- /dev/null
+++ b/pallets/membership/src/lib.rs
@@ -0,0 +1,509 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![allow(clippy::type_complexity)]
+
+pub mod traits;
+pub mod types;
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+/*#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;*/
+
+pub use pallet::*;
+
+use crate::traits::*;
+use crate::types::{MembershipData, OriginPermission};
+use frame_support::dispatch::Weight;
+use frame_support::pallet_prelude::DispatchResultWithPostInfo;
+use sp_runtime::traits::Zero;
+use sp_std::prelude::*;
+#[cfg(feature = "std")]
+use std::collections::BTreeMap;
+
+#[frame_support::pallet]
+pub mod pallet {
+    use super::*;
+    use frame_support::pallet_prelude::*;
+    use frame_support::traits::StorageVersion;
+    use frame_system::pallet_prelude::*;
+
+    /// The current storage version.
+    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
+
+    #[pallet::pallet]
+    #[pallet::generate_store(pub(super) trait Store)]
+    #[pallet::storage_version(STORAGE_VERSION)]
+    pub struct Pallet<T, I = ()>(_);
+
+    // CONFIG //
+
+    #[pallet::config]
+    pub trait Config<I: 'static = ()>: frame_system::Config {
+        type IsIdtyAllowedToClaimMembership: IsIdtyAllowedToClaimMembership<Self::IdtyId>;
+        type IsIdtyAllowedToRenewMembership: IsIdtyAllowedToRenewMembership<Self::IdtyId>;
+        type IsIdtyAllowedToRequestMembership: IsIdtyAllowedToRequestMembership<Self::IdtyId>;
+        type IsOriginAllowedToUseIdty: IsOriginAllowedToUseIdty<Self::Origin, Self::IdtyId>;
+        /// Because this pallet emits events, it depends on the runtime's definition of an event.
+        type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>;
+        /// Specify true if you want to externalize the storage of memberships, but in this case
+        /// you must provide an implementation of `MembershipExternalStorage`
+        type ExternalizeMembershipStorage: Get<bool>;
+        /// Something that identifies an identity
+        type IdtyId: Copy + MaybeSerializeDeserialize + Parameter + Ord;
+        /// On event handler
+        type OnEvent: OnEvent<Self::IdtyId>;
+        /// Provide your implementation of membership storage here, if you want the pallet to
+        /// handle the storage for you, specify `()` and set `ExternalizeMembershipStorage` to
+        /// `false`.
+        type MembershipExternalStorage: MembershipExternalStorage<Self::BlockNumber, Self::IdtyId>;
+        #[pallet::constant]
+        /// Maximum life span of a non-renewable membership (in number of blocks)
+        type MembershipPeriod: Get<Self::BlockNumber>;
+        #[pallet::constant]
+        /// Maximum period (in number of blocks), where an identity can remain pending subscription.
+        type PendingMembershipPeriod: Get<Self::BlockNumber>;
+        #[pallet::constant]
+        /// Duration after which a membership is renewable
+        type RenewablePeriod: Get<Self::BlockNumber>;
+        #[pallet::constant]
+        /// Minimum duration (in number of blocks between a revocation and a new entry request
+        type RevocationPeriod: Get<Self::BlockNumber>;
+    }
+
+    // GENESIS STUFFĂ‚ //
+
+    #[pallet::genesis_config]
+    pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
+        pub memberships: BTreeMap<T::IdtyId, MembershipData<T::BlockNumber>>,
+    }
+
+    #[cfg(feature = "std")]
+    impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
+        fn default() -> Self {
+            Self {
+                memberships: Default::default(),
+            }
+        }
+    }
+
+    #[pallet::genesis_build]
+    impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> {
+        fn build(&self) {
+            for (idty_id, membership_data) in &self.memberships {
+                MembershipsExpireOn::<T, I>::append(membership_data.expire_on, idty_id);
+                if T::ExternalizeMembershipStorage::get() {
+                    T::MembershipExternalStorage::insert(*idty_id, *membership_data);
+                } else {
+                    Membership::<T, I>::insert(idty_id, membership_data);
+                }
+            }
+        }
+    }
+
+    // STORAGE //
+
+    #[pallet::storage]
+    #[pallet::getter(fn membership)]
+    pub type Membership<T: Config<I>, I: 'static = ()> =
+        StorageMap<_, Blake2_128Concat, T::IdtyId, MembershipData<T::BlockNumber>, OptionQuery>;
+
+    #[pallet::storage]
+    #[pallet::getter(fn memberships_expire_on)]
+    pub type MembershipsExpireOn<T: Config<I>, I: 'static = ()> =
+        StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>;
+
+    #[pallet::storage]
+    #[pallet::getter(fn pending_membership)]
+    pub type PendingMembership<T: Config<I>, I: 'static = ()> =
+        StorageMap<_, Blake2_128Concat, T::IdtyId, (), OptionQuery>;
+
+    #[pallet::storage]
+    #[pallet::getter(fn pending_memberships_expire_on)]
+    pub type PendingMembershipsExpireOn<T: Config<I>, I: 'static = ()> =
+        StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>;
+
+    #[pallet::storage]
+    #[pallet::getter(fn revoked_membership)]
+    pub type RevokedMembership<T: Config<I>, I: 'static = ()> =
+        StorageMap<_, Blake2_128Concat, T::IdtyId, (), OptionQuery>;
+
+    #[pallet::storage]
+    #[pallet::getter(fn revoked_memberships_pruned_on)]
+    pub type RevokedMembershipsPrunedOn<T: Config<I>, I: 'static = ()> =
+        StorageMap<_, Blake2_128Concat, T::BlockNumber, Vec<T::IdtyId>, OptionQuery>;
+
+    // EVENTS //
+
+    #[pallet::event]
+    #[pallet::generate_deposit(pub(super) fn deposit_event)]
+    pub enum Event<T: Config<I>, I: 'static = ()> {
+        /// A membership has acquired
+        /// [idty_id]
+        MembershipAcquired(T::IdtyId),
+        /// A membership has expired
+        /// [idty_id]
+        MembershipExpired(T::IdtyId),
+        /// A membership has renewed
+        /// [idty_id]
+        MembershipRenewed(T::IdtyId),
+        /// An identity requested membership
+        /// [idty_id]
+        MembershipRequested(T::IdtyId),
+        /// A membership has revoked
+        /// [idty_id]
+        MembershipRevoked(T::IdtyId),
+        /// A pending membership request has expired
+        /// [idty_id]
+        PendingMembershipExpired(T::IdtyId),
+    }
+
+    // ERRORS//
+
+    #[pallet::error]
+    pub enum Error<T, I = ()> {
+        /// Identity not allowed to claim membership
+        IdtyNotAllowedToClaimMembership,
+        /// Identity not allowed to renew membership
+        IdtyNotAllowedToRenewMembership,
+        /// Identity not allowed to request membership
+        IdtyNotAllowedToRequestMembership,
+        /// Origin not allowed to use this identity
+        /// Membership not yet renewable
+        MembershipNotYetRenewable,
+        /// Membership not found
+        MembershipNotFound,
+        OriginNotAllowedToUseIdty,
+        /// Membership request not found
+        MembershipRequestNotFound,
+        /// Membership revoked recently
+        MembershipRevokedRecently,
+    }
+
+    // HOOKS //
+
+    #[pallet::hooks]
+    impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
+        fn on_initialize(n: T::BlockNumber) -> Weight {
+            if n > T::BlockNumber::zero() {
+                Self::expire_pending_memberships(n)
+                    + Self::expire_memberships(n)
+                    + Self::prune_revoked_memberships(n)
+            } else {
+                0
+            }
+        }
+    }
+
+    // CALLS //
+
+    #[pallet::call]
+    impl<T: Config<I>, I: 'static> Pallet<T, I> {
+        #[pallet::weight(0)]
+        pub fn request_membership(
+            origin: OriginFor<T>,
+            idty_id: T::IdtyId,
+        ) -> DispatchResultWithPostInfo {
+            let allowed =
+                match T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id)
+                {
+                    OriginPermission::Forbidden => {
+                        return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into())
+                    }
+                    OriginPermission::Allowed => {
+                        T::IsIdtyAllowedToRequestMembership::is_idty_allowed_to_request_membership(
+                            &idty_id,
+                        )
+                    }
+                    OriginPermission::Root => true,
+                };
+            if !allowed {
+                return Err(Error::<T, I>::IdtyNotAllowedToRequestMembership.into());
+            }
+            if RevokedMembership::<T, I>::contains_key(&idty_id) {
+                return Err(Error::<T, I>::MembershipRevokedRecently.into());
+            }
+
+            let block_number = frame_system::pallet::Pallet::<T>::block_number();
+            let expire_on = block_number + T::PendingMembershipPeriod::get();
+
+            PendingMembership::<T, I>::insert(idty_id, ());
+            PendingMembershipsExpireOn::<T, I>::append(expire_on, idty_id);
+            Self::deposit_event(Event::MembershipRequested(idty_id));
+            T::OnEvent::on_event(crate::types::Event::MembershipRequested(idty_id));
+
+            Ok(().into())
+        }
+
+        #[pallet::weight(0)]
+        pub fn claim_membership(
+            origin: OriginFor<T>,
+            idty_id: T::IdtyId,
+        ) -> DispatchResultWithPostInfo {
+            let allowed =
+                match T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id)
+                {
+                    OriginPermission::Forbidden => {
+                        return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into())
+                    }
+                    OriginPermission::Allowed => {
+                        T::IsIdtyAllowedToClaimMembership::is_idty_allowed_to_claim_membership(
+                            &idty_id,
+                        )
+                    }
+                    OriginPermission::Root => true,
+                };
+            if !allowed {
+                return Err(Error::<T, I>::IdtyNotAllowedToClaimMembership.into());
+            }
+
+            if !PendingMembership::<T, I>::contains_key(&idty_id) {
+                return Err(Error::<T, I>::MembershipRequestNotFound.into());
+            }
+
+            let _ = Self::do_claim_membership(idty_id);
+
+            Ok(().into())
+        }
+
+        #[pallet::weight(0)]
+        pub fn renew_membership(
+            origin: OriginFor<T>,
+            idty_id: T::IdtyId,
+        ) -> DispatchResultWithPostInfo {
+            let allowed =
+                match T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id)
+                {
+                    OriginPermission::Forbidden => {
+                        return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into())
+                    }
+                    OriginPermission::Allowed => {
+                        T::IsIdtyAllowedToRenewMembership::is_idty_allowed_to_renew_membership(
+                            &idty_id,
+                        )
+                    }
+                    OriginPermission::Root => true,
+                };
+            if !allowed {
+                return Err(Error::<T, I>::IdtyNotAllowedToRenewMembership.into());
+            }
+
+            if let Some(membership_data) = Self::get_membership(&idty_id) {
+                let block_number = frame_system::pallet::Pallet::<T>::block_number();
+                if membership_data.renewable_on > block_number {
+                    return Err(Error::<T, I>::MembershipNotYetRenewable.into());
+                }
+            } else {
+                return Err(Error::<T, I>::MembershipNotFound.into());
+            }
+
+            let _ = Self::do_renew_membership(idty_id);
+
+            Ok(().into())
+        }
+
+        #[pallet::weight(0)]
+        pub fn revoke_membership(
+            origin: OriginFor<T>,
+            idty_id: T::IdtyId,
+        ) -> DispatchResultWithPostInfo {
+            if T::IsOriginAllowedToUseIdty::is_origin_allowed_to_use_idty(&origin, &idty_id)
+                == OriginPermission::Forbidden
+            {
+                return Err(Error::<T, I>::OriginNotAllowedToUseIdty.into());
+            }
+
+            let _ = Self::do_revoke_membership(idty_id);
+
+            Ok(().into())
+        }
+    }
+
+    // INTERNAL FUNCTIONS //
+
+    impl<T: Config<I>, I: 'static> Pallet<T, I> {
+        pub(super) fn do_claim_membership(idty_id: T::IdtyId) -> Weight {
+            let mut total_weight = 1;
+            PendingMembership::<T, I>::remove(&idty_id);
+            total_weight += Self::do_renew_membership_inner(idty_id);
+            Self::deposit_event(Event::MembershipAcquired(idty_id));
+            T::OnEvent::on_event(crate::types::Event::MembershipAcquired(idty_id));
+            total_weight
+        }
+        pub(super) fn do_renew_membership(idty_id: T::IdtyId) -> Weight {
+            let total_weight = Self::do_renew_membership_inner(idty_id);
+            Self::deposit_event(Event::MembershipRenewed(idty_id));
+            T::OnEvent::on_event(crate::types::Event::MembershipRenewed(idty_id));
+            total_weight
+        }
+        fn do_renew_membership_inner(idty_id: T::IdtyId) -> Weight {
+            let block_number = frame_system::pallet::Pallet::<T>::block_number();
+            let expire_on = block_number + T::MembershipPeriod::get();
+            let renewable_on = block_number + T::RenewablePeriod::get();
+
+            Self::insert_membership(
+                idty_id,
+                MembershipData {
+                    expire_on,
+                    renewable_on,
+                },
+            );
+            MembershipsExpireOn::<T, I>::append(expire_on, idty_id);
+            0
+        }
+        pub(super) fn do_revoke_membership(idty_id: T::IdtyId) -> Weight {
+            let block_number = frame_system::pallet::Pallet::<T>::block_number();
+            let pruned_on = block_number + T::RevocationPeriod::get();
+
+            Self::remove_membership(&idty_id);
+            RevokedMembership::<T, I>::insert(idty_id, ());
+            RevokedMembershipsPrunedOn::<T, I>::append(pruned_on, idty_id);
+            Self::deposit_event(Event::MembershipRevoked(idty_id));
+            T::OnEvent::on_event(crate::types::Event::MembershipRevoked(idty_id));
+            0
+        }
+        fn expire_memberships(block_number: T::BlockNumber) -> Weight {
+            let mut total_weight: Weight = 0;
+
+            use frame_support::storage::generator::StorageMap as _;
+            if let Some(identities_ids) = MembershipsExpireOn::<T, I>::from_query_to_optional_value(
+                MembershipsExpireOn::<T, I>::take(block_number),
+            ) {
+                for idty_id in identities_ids {
+                    if let Some(member_data) = Self::get_membership(&idty_id) {
+                        if member_data.expire_on == block_number {
+                            Self::remove_membership(&idty_id);
+                            Self::deposit_event(Event::MembershipExpired(idty_id));
+                            total_weight += T::OnEvent::on_event(
+                                crate::types::Event::MembershipExpired(idty_id),
+                            );
+                        }
+                    }
+                }
+            }
+
+            total_weight
+        }
+        fn expire_pending_memberships(block_number: T::BlockNumber) -> Weight {
+            let mut total_weight: Weight = 0;
+
+            use frame_support::storage::generator::StorageMap as _;
+            if let Some(identities_ids) =
+                PendingMembershipsExpireOn::<T, I>::from_query_to_optional_value(
+                    PendingMembershipsExpireOn::<T, I>::take(block_number),
+                )
+            {
+                for idty_id in identities_ids {
+                    PendingMembership::<T, I>::remove(&idty_id);
+                    Self::deposit_event(Event::PendingMembershipExpired(idty_id));
+                    total_weight += T::OnEvent::on_event(
+                        crate::types::Event::PendingMembershipExpired(idty_id),
+                    );
+                }
+            }
+
+            total_weight
+        }
+        fn prune_revoked_memberships(block_number: T::BlockNumber) -> Weight {
+            let total_weight: Weight = 0;
+
+            use frame_support::storage::generator::StorageMap as _;
+            if let Some(identities_ids) =
+                RevokedMembershipsPrunedOn::<T, I>::from_query_to_optional_value(
+                    RevokedMembershipsPrunedOn::<T, I>::take(block_number),
+                )
+            {
+                for idty_id in identities_ids {
+                    RevokedMembership::<T, I>::remove(idty_id);
+                }
+            }
+
+            total_weight
+        }
+
+        pub(super) fn is_member_inner(idty_id: &T::IdtyId) -> bool {
+            if T::ExternalizeMembershipStorage::get() {
+                T::MembershipExternalStorage::is_member(idty_id)
+            } else {
+                Membership::<T, I>::contains_key(idty_id)
+            }
+        }
+        fn insert_membership(idty_id: T::IdtyId, membership_data: MembershipData<T::BlockNumber>) {
+            if T::ExternalizeMembershipStorage::get() {
+                T::MembershipExternalStorage::insert(idty_id, membership_data);
+            } else {
+                Membership::<T, I>::insert(idty_id, membership_data);
+            }
+        }
+        fn get_membership(idty_id: &T::IdtyId) -> Option<MembershipData<T::BlockNumber>> {
+            if T::ExternalizeMembershipStorage::get() {
+                T::MembershipExternalStorage::get(idty_id)
+            } else {
+                Membership::<T, I>::try_get(idty_id).ok()
+            }
+        }
+        fn remove_membership(idty_id: &T::IdtyId) {
+            if T::ExternalizeMembershipStorage::get() {
+                T::MembershipExternalStorage::remove(idty_id);
+            } else {
+                Membership::<T, I>::remove(idty_id);
+            }
+        }
+    }
+}
+
+impl<T: Config<I>, I: 'static> IsInPendingMemberships<T::IdtyId> for Pallet<T, I> {
+    fn is_in_pending_memberships(idty_id: T::IdtyId) -> bool {
+        PendingMembership::<T, I>::contains_key(idty_id)
+    }
+}
+
+impl<T: Config<I>, I: 'static> IsMember<T::IdtyId> for Pallet<T, I> {
+    fn is_member(idty_id: &T::IdtyId) -> bool {
+        Self::is_member_inner(&idty_id)
+    }
+}
+
+impl<T: Config<I>, I: 'static> MembershipAction<T::IdtyId, T::Origin> for Pallet<T, I> {
+    fn request_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo {
+        Pallet::<T, I>::request_membership(origin, idty_id)
+    }
+    fn claim_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo {
+        Pallet::<T, I>::claim_membership(origin, idty_id)
+    }
+    fn renew_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo {
+        Pallet::<T, I>::renew_membership(origin, idty_id)
+    }
+    fn revoke_membership_(origin: T::Origin, idty_id: T::IdtyId) -> DispatchResultWithPostInfo {
+        Pallet::<T, I>::revoke_membership(origin, idty_id)
+    }
+
+    fn force_claim_membership(idty_id: T::IdtyId) -> Weight {
+        Self::do_claim_membership(idty_id)
+    }
+    fn force_renew_membership(idty_id: T::IdtyId) -> Weight {
+        Self::do_renew_membership(idty_id)
+    }
+    fn force_revoke_membership(idty_id: T::IdtyId) -> Weight {
+        Self::do_revoke_membership(idty_id)
+    }
+}
diff --git a/pallets/membership/src/mock.rs b/pallets/membership/src/mock.rs
new file mode 100644
index 000000000..237d93a27
--- /dev/null
+++ b/pallets/membership/src/mock.rs
@@ -0,0 +1,139 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
+
+use crate::traits::IsOriginAllowedToUseIdty;
+use crate::types::OriginPermission;
+use crate::{self as pallet_membership};
+use frame_support::{
+    parameter_types,
+    traits::{Everything, OnFinalize, OnInitialize},
+};
+use frame_system as system;
+use sp_core::H256;
+use sp_runtime::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    BuildStorage,
+};
+
+type AccountId = u64;
+type BlockNumber = u64;
+type Block = frame_system::mocking::MockBlock<Test>;
+pub type IdtyId = u64;
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
+
+// Configure a mock runtime to test the pallet.
+frame_support::construct_runtime!(
+    pub enum Test where
+        Block = Block,
+        NodeBlock = Block,
+        UncheckedExtrinsic = UncheckedExtrinsic,
+    {
+        System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
+        DefaultMembership: pallet_membership::{Pallet, Call, Event<T>, Storage, Config<T>},
+    }
+);
+
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const SS58Prefix: u8 = 42;
+}
+
+impl system::Config for Test {
+    type BaseCallFilter = Everything;
+    type BlockWeights = ();
+    type BlockLength = ();
+    type DbWeight = ();
+    type Origin = Origin;
+    type Call = Call;
+    type Index = u64;
+    type BlockNumber = BlockNumber;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = AccountId;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type Event = Event;
+    type BlockHashCount = BlockHashCount;
+    type Version = ();
+    type PalletInfo = PalletInfo;
+    type AccountData = ();
+    type OnNewAccount = ();
+    type OnKilledAccount = ();
+    type SystemWeightInfo = ();
+    type SS58Prefix = SS58Prefix;
+    type OnSetCode = ();
+    type MaxConsumers = frame_support::traits::ConstU32<16>;
+}
+
+pub struct IsOriginAllowedToUseIdtyImpl;
+impl IsOriginAllowedToUseIdty<Origin, IdtyId> for IsOriginAllowedToUseIdtyImpl {
+    fn is_origin_allowed_to_use_idty(o: &Origin, idty_id: &IdtyId) -> OriginPermission {
+        match o.clone().into() {
+            Ok(system::RawOrigin::Root) => OriginPermission::Root,
+            Ok(system::RawOrigin::Signed(account_id)) if account_id == *idty_id => {
+                OriginPermission::Allowed
+            }
+            _ => OriginPermission::Forbidden,
+        }
+    }
+}
+
+parameter_types! {
+    pub const ExternalizeMembershipStorage: bool = false;
+    pub const MembershipPeriod: BlockNumber = 5;
+    pub const PendingMembershipPeriod: BlockNumber = 3;
+    pub const RenewablePeriod: BlockNumber = 2;
+    pub const RevocationPeriod: BlockNumber = 4;
+}
+
+impl pallet_membership::Config for Test {
+    type IsIdtyAllowedToClaimMembership = ();
+    type IsIdtyAllowedToRenewMembership = ();
+    type IsIdtyAllowedToRequestMembership = ();
+    type IsOriginAllowedToUseIdty = IsOriginAllowedToUseIdtyImpl;
+    type Event = Event;
+    type ExternalizeMembershipStorage = ExternalizeMembershipStorage;
+    type IdtyId = IdtyId;
+    type OnEvent = ();
+    type MembershipExternalStorage = ();
+    type MembershipPeriod = MembershipPeriod;
+    type PendingMembershipPeriod = PendingMembershipPeriod;
+    type RenewablePeriod = RenewablePeriod;
+    type RevocationPeriod = RevocationPeriod;
+}
+
+// Build genesis storage according to the mock runtime.
+pub fn new_test_ext(gen_conf: pallet_membership::GenesisConfig<Test>) -> sp_io::TestExternalities {
+    GenesisConfig {
+        system: SystemConfig::default(),
+        default_membership: gen_conf,
+    }
+    .build_storage()
+    .unwrap()
+    .into()
+}
+
+pub fn run_to_block(n: u64) {
+    while System::block_number() < n {
+        DefaultMembership::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());
+        DefaultMembership::on_initialize(System::block_number());
+    }
+}
diff --git a/pallets/membership/src/tests.rs b/pallets/membership/src/tests.rs
new file mode 100644
index 000000000..f98e49909
--- /dev/null
+++ b/pallets/membership/src/tests.rs
@@ -0,0 +1,199 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
+
+use crate::mock::Event as RuntimeEvent;
+use crate::mock::*;
+use crate::traits::{IsInPendingMemberships, IsMember};
+use crate::types::MembershipData;
+use crate::{Error, Event};
+use frame_support::assert_ok;
+use maplit::btreemap;
+
+fn default_gen_conf() -> DefaultMembershipConfig {
+    DefaultMembershipConfig {
+        memberships: btreemap![
+            0 => MembershipData {
+                expire_on: 3,
+                renewable_on: 2
+            }
+        ],
+    }
+}
+
+#[test]
+fn test_genesis_build() {
+    new_test_ext(default_gen_conf()).execute_with(|| {
+        run_to_block(1);
+        // Verify state
+        assert_eq!(
+            DefaultMembership::membership(0),
+            Some(MembershipData {
+                expire_on: 3,
+                renewable_on: 2
+            })
+        );
+    });
+}
+
+#[test]
+fn test_membership_not_yet_renewable() {
+    new_test_ext(default_gen_conf()).execute_with(|| {
+        run_to_block(1);
+        // Merbership 0 cannot be renewed before #2
+        assert_eq!(
+            DefaultMembership::renew_membership(Origin::signed(0), 0),
+            Err(Error::<Test, _>::MembershipNotYetRenewable.into())
+        );
+    });
+}
+
+#[test]
+fn test_membership_request_not_found() {
+    new_test_ext(default_gen_conf()).execute_with(|| {
+        run_to_block(1);
+        // Merbership 0 cannot be reclaimed
+        assert_eq!(
+            DefaultMembership::claim_membership(Origin::signed(0), 0),
+            Err(Error::<Test, _>::MembershipRequestNotFound.into())
+        );
+    });
+}
+
+#[test]
+fn test_membership_renewal() {
+    new_test_ext(default_gen_conf()).execute_with(|| {
+        run_to_block(2);
+        // Merbership 0 can be renewable on block #2
+        assert_ok!(DefaultMembership::renew_membership(Origin::signed(0), 0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipRenewed(0))
+        );
+    });
+}
+
+#[test]
+fn test_membership_expiration() {
+    new_test_ext(default_gen_conf()).execute_with(|| {
+        // Merbership 0 should not expired on block #2
+        run_to_block(2);
+        assert!(DefaultMembership::is_member(&0),);
+        // Merbership 0 should expire on block #3
+        run_to_block(3);
+        assert!(!DefaultMembership::is_member(&0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipExpired(0))
+        );
+    });
+}
+
+#[test]
+fn test_membership_revocation() {
+    new_test_ext(default_gen_conf()).execute_with(|| {
+        run_to_block(1);
+        // Merbership 0 can be revocable on block #1
+        assert_ok!(DefaultMembership::revoke_membership(Origin::signed(0), 0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipRevoked(0))
+        );
+
+        // Membership 0 can't request membership before the end of RevokePeriod (1 + 4 = 5)
+        run_to_block(2);
+        assert_eq!(
+            DefaultMembership::request_membership(Origin::signed(0), 0),
+            Err(Error::<Test, _>::MembershipRevokedRecently.into())
+        );
+
+        // Membership 0 can request membership after the end of RevokePeriod (1 + 4 = 5)
+        run_to_block(5);
+        assert_ok!(DefaultMembership::request_membership(Origin::signed(0), 0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
+        );
+    });
+}
+
+#[test]
+fn test_pending_membership_expiration() {
+    new_test_ext(Default::default()).execute_with(|| {
+        // Idty 0 request membership
+        run_to_block(1);
+        assert_ok!(DefaultMembership::request_membership(Origin::signed(0), 0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
+        );
+
+        // Then, idty 0 shold still in pending memberships until PendingMembershipPeriod ended
+        run_to_block(PendingMembershipPeriod::get());
+        assert!(DefaultMembership::is_in_pending_memberships(0),);
+
+        // Then, idty 0 request should expire after PendingMembershipPeriod
+        run_to_block(1 + PendingMembershipPeriod::get());
+        assert!(!DefaultMembership::is_in_pending_memberships(0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::PendingMembershipExpired(0))
+        );
+    })
+}
+
+#[test]
+fn test_membership_workflow() {
+    new_test_ext(Default::default()).execute_with(|| {
+        // Idty 0 request membership
+        run_to_block(1);
+        assert_ok!(DefaultMembership::request_membership(Origin::signed(0), 0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipRequested(0))
+        );
+
+        // Then, idty 0 claim membership
+        run_to_block(2);
+        assert_ok!(DefaultMembership::claim_membership(Origin::signed(0), 0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipAcquired(0))
+        );
+
+        // Then, idty 0 claim renewal, should fail
+        run_to_block(3);
+        assert_eq!(
+            DefaultMembership::renew_membership(Origin::signed(0), 0),
+            Err(Error::<Test, _>::MembershipNotYetRenewable.into())
+        );
+
+        // Then, idty 0 claim renewal after renewable period, should success
+        run_to_block(2 + RenewablePeriod::get());
+        assert_ok!(DefaultMembership::renew_membership(Origin::signed(0), 0),);
+
+        // Then, idty 0 shoul still member until membership period ended
+        run_to_block(2 + RenewablePeriod::get() + MembershipPeriod::get() - 1);
+        assert!(DefaultMembership::is_member(&0));
+
+        // Then, idty 0 shoul expire after membership period
+        run_to_block(2 + RenewablePeriod::get() + MembershipPeriod::get());
+        assert!(!DefaultMembership::is_member(&0),);
+        assert_eq!(
+            System::events()[0].event,
+            RuntimeEvent::DefaultMembership(Event::MembershipExpired(0))
+        );
+    });
+}
diff --git a/pallets/membership/src/traits.rs b/pallets/membership/src/traits.rs
new file mode 100644
index 000000000..c0360b201
--- /dev/null
+++ b/pallets/membership/src/traits.rs
@@ -0,0 +1,119 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
+
+use crate::types::OriginPermission;
+use crate::*;
+use frame_support::pallet_prelude::DispatchResultWithPostInfo;
+
+pub trait IsIdtyAllowedToClaimMembership<IdtyId> {
+    fn is_idty_allowed_to_claim_membership(idty_id: &IdtyId) -> bool;
+}
+
+impl<IdtyId> IsIdtyAllowedToClaimMembership<IdtyId> for () {
+    fn is_idty_allowed_to_claim_membership(_: &IdtyId) -> bool {
+        true
+    }
+}
+
+pub trait IsIdtyAllowedToRenewMembership<IdtyId> {
+    fn is_idty_allowed_to_renew_membership(idty_id: &IdtyId) -> bool;
+}
+
+impl<IdtyId> IsIdtyAllowedToRenewMembership<IdtyId> for () {
+    fn is_idty_allowed_to_renew_membership(_: &IdtyId) -> bool {
+        true
+    }
+}
+
+pub trait IsIdtyAllowedToRequestMembership<IdtyId> {
+    fn is_idty_allowed_to_request_membership(idty_id: &IdtyId) -> bool;
+}
+
+impl<IdtyId> IsIdtyAllowedToRequestMembership<IdtyId> for () {
+    fn is_idty_allowed_to_request_membership(_: &IdtyId) -> bool {
+        true
+    }
+}
+
+pub trait IsOriginAllowedToUseIdty<Origin, IdtyId> {
+    fn is_origin_allowed_to_use_idty(origin: &Origin, idty_id: &IdtyId) -> OriginPermission;
+}
+
+impl<Origin, IdtyId> IsOriginAllowedToUseIdty<Origin, IdtyId> for () {
+    fn is_origin_allowed_to_use_idty(_: &Origin, _: &IdtyId) -> OriginPermission {
+        OriginPermission::Allowed
+    }
+}
+
+pub trait IsInPendingMemberships<IdtyId> {
+    fn is_in_pending_memberships(idty_id: IdtyId) -> bool;
+}
+
+pub trait IsMember<IdtyId> {
+    fn is_member(idty_id: &IdtyId) -> bool;
+}
+
+impl<IdtyId> IsMember<IdtyId> for () {
+    fn is_member(_: &IdtyId) -> bool {
+        false
+    }
+}
+
+pub trait OnEvent<IdtyId> {
+    fn on_event(event: crate::types::Event<IdtyId>) -> Weight;
+}
+
+impl<IdtyId> OnEvent<IdtyId> for () {
+    fn on_event(_: crate::types::Event<IdtyId>) -> Weight {
+        0
+    }
+}
+
+pub trait MembershipAction<IdtyId, Origin> {
+    fn request_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo;
+    fn claim_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo;
+    fn renew_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo;
+    fn revoke_membership_(origin: Origin, idty_id: IdtyId) -> DispatchResultWithPostInfo;
+    fn force_claim_membership(idty_id: IdtyId) -> Weight;
+    fn force_renew_membership(idty_id: IdtyId) -> Weight;
+    fn force_revoke_membership(idty_id: IdtyId) -> Weight;
+}
+
+pub trait MembershipExternalStorage<BlockNumber: Decode + Encode + TypeInfo, IdtyId>:
+    IsMember<IdtyId>
+{
+    fn insert(idty_id: IdtyId, membership_data: MembershipData<BlockNumber>);
+    fn get(idty_id: &IdtyId) -> Option<MembershipData<BlockNumber>>;
+    fn remove(idty_id: &IdtyId);
+}
+
+use codec::{Decode, Encode};
+use frame_support::pallet_prelude::TypeInfo;
+static INVALID_CONF_MSG: &str = "invalid pallet configuration: if `MembershipExternalStorage` = (), you must set `ExternalizeMembershipStorage` to `false`.";
+
+impl<BlockNumber: Decode + Encode + TypeInfo, IdtyId> MembershipExternalStorage<BlockNumber, IdtyId>
+    for ()
+{
+    fn insert(_: IdtyId, _: MembershipData<BlockNumber>) {
+        panic!("{}", INVALID_CONF_MSG)
+    }
+    fn get(_: &IdtyId) -> Option<MembershipData<BlockNumber>> {
+        panic!("{}", INVALID_CONF_MSG)
+    }
+    fn remove(_: &IdtyId) {
+        panic!("{}", INVALID_CONF_MSG)
+    }
+}
diff --git a/pallets/membership/src/types.rs b/pallets/membership/src/types.rs
new file mode 100644
index 000000000..5eac49506
--- /dev/null
+++ b/pallets/membership/src/types.rs
@@ -0,0 +1,58 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Substrate-Libre-Currency.
+//
+// Substrate-Libre-Currency 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.
+//
+// Substrate-Libre-Currency 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
+
+//! Various basic types for use in the membership pallet.
+
+use codec::{Decode, Encode};
+use frame_support::RuntimeDebug;
+use scale_info::TypeInfo;
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+pub enum Event<IdtyId> {
+    /// A membership has acquired
+    /// [idty_id]
+    MembershipAcquired(IdtyId),
+    /// A membership has expired
+    /// [idty_id]
+    MembershipExpired(IdtyId),
+    /// A membership has renewed
+    /// [idty_id]
+    MembershipRenewed(IdtyId),
+    /// An identity requested membership
+    /// [idty_id]
+    MembershipRequested(IdtyId),
+    /// A membership has revoked
+    /// [idty_id]
+    MembershipRevoked(IdtyId),
+    /// A pending membership request has expired
+    /// [idty_id]
+    PendingMembershipExpired(IdtyId),
+}
+
+#[derive(PartialEq)]
+pub enum OriginPermission {
+    Allowed,
+    Forbidden,
+    Root,
+}
+
+#[cfg_attr(feature = "std", derive(Deserialize, Serialize))]
+#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)]
+pub struct MembershipData<BlockNumber: Decode + Encode + TypeInfo> {
+    pub expire_on: BlockNumber,
+    pub renewable_on: BlockNumber,
+}
diff --git a/pallets/ud-accounts-storage/Cargo.toml b/pallets/ud-accounts-storage/Cargo.toml
index 2d337b8b4..4bf8ce942 100644
--- a/pallets/ud-accounts-storage/Cargo.toml
+++ b/pallets/ud-accounts-storage/Cargo.toml
@@ -6,7 +6,7 @@ homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'pallet-ud-accounts-storage'
 readme = 'README.md'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 
 [features]
diff --git a/pallets/universal-dividend/Cargo.toml b/pallets/universal-dividend/Cargo.toml
index 59e4e399a..9fc29a57e 100644
--- a/pallets/universal-dividend/Cargo.toml
+++ b/pallets/universal-dividend/Cargo.toml
@@ -5,7 +5,7 @@ edition = '2018'
 homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'pallet-universal-dividend'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 
 [features]
diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml
index 8750b7412..6046607d3 100644
--- a/runtime/g1/Cargo.toml
+++ b/runtime/g1/Cargo.toml
@@ -8,7 +8,7 @@ edition = '2018'
 homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'g1-runtime'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 [package.metadata.docs.rs]
 targets = ['x86_64-unknown-linux-gnu']
diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml
index 59bae5890..d8d6ce509 100644
--- a/runtime/gdev/Cargo.toml
+++ b/runtime/gdev/Cargo.toml
@@ -8,7 +8,7 @@ edition = '2018'
 homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'gdev-runtime'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 [package.metadata.docs.rs]
 targets = ['x86_64-unknown-linux-gnu']
diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml
index c102d90fa..707a56c0a 100644
--- a/runtime/gtest/Cargo.toml
+++ b/runtime/gtest/Cargo.toml
@@ -8,7 +8,7 @@ edition = '2018'
 homepage = 'https://substrate.dev'
 license = 'AGPL-3.0'
 name = 'gtest-runtime'
-repository = 'https://git.duniter.org/nodes/rust/duniter-substrate'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
 version = '3.0.0'
 [package.metadata.docs.rs]
 targets = ['x86_64-unknown-linux-gnu']
-- 
GitLab