Browse Source

Merge pull request #1953 from shamil-gadelshin/membership_staking_accounts

Add staking accounts to the membership pallet.
Bedeho Mender 4 years ago
parent
commit
77239c78d9

+ 0 - 1
Cargo.lock

@@ -7213,7 +7213,6 @@ dependencies = [
  "frame-system",
  "pallet-balances",
  "pallet-common",
- "pallet-membership",
  "pallet-timestamp",
  "parity-scale-codec",
  "sp-arithmetic",

+ 6 - 0
runtime-modules/common/src/lib.rs

@@ -42,6 +42,12 @@ pub trait Trait: frame_system::Trait {
         + PartialEq;
 }
 
+/// Validates staking account ownership for a member.
+pub trait StakingAccountValidator<T: Trait> {
+    /// Verifies that staking account bound to the member.
+    fn is_member_staking_account(member_id: &MemberId<T>, account_id: &T::AccountId) -> bool;
+}
+
 /// Defines time in both block number and substrate time abstraction.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Clone, Encode, Decode, PartialEq, Eq, Debug, Default)]

+ 154 - 1
runtime-modules/membership/src/lib.rs

@@ -89,7 +89,7 @@ pub type Membership<T> = MembershipObject<<T as frame_system::Trait>::AccountId>
 
 #[derive(Encode, Decode, Default)]
 /// Stored information about a registered user.
-pub struct MembershipObject<AccountId> {
+pub struct MembershipObject<AccountId: Ord> {
     /// User name.
     pub name: Vec<u8>,
 
@@ -121,6 +121,16 @@ pub struct MembershipObject<AccountId> {
     pub invites: u32,
 }
 
+// Contain staking account to member binding and its confirmation.
+#[derive(Encode, Decode, Default)]
+pub struct StakingAccountMemberBinding<MemberId> {
+    /// Member id that we bind account to.
+    pub member_id: MemberId,
+
+    /// Confirmation that an account id is bound to a member.
+    pub confirmed: bool,
+}
+
 // Contains valid or default user details
 struct ValidatedUserInfo {
     name: Vec<u8>,
@@ -226,6 +236,15 @@ decl_error! {
 
         /// Membership working group leader is not set.
         WorkingGroupLeaderNotSet,
+
+        /// Staking account is registered for some member.
+        StakingAccountIsAlreadyRegistered,
+
+        /// Staking account for membership doesn't exist.
+        StakingAccountDoesntExist,
+
+        /// Staking account has already been confirmed.
+        StakingAccountAlreadyConfirmed,
     }
 }
 
@@ -280,6 +299,11 @@ decl_storage! {
         /// Initial invitation balance for the invited member.
         pub InitialInvitationBalance get(fn initial_invitation_balance) : BalanceOf<T> =
             T::DefaultInitialInvitationBalance::get();
+
+        /// Double of a staking account id and member id to the confirmation status.
+        pub(crate) StakingAccountIdMemberStatus get(fn staking_account_id_member_status):
+            map hasher(blake2_128_concat) T::AccountId => StakingAccountMemberBinding<T::MemberId>;
+
     }
     add_extra_genesis {
         config(members) : Vec<genesis::Member<T::MemberId, T::AccountId>>;
@@ -310,6 +334,7 @@ decl_event! {
     pub enum Event<T> where
       <T as common::Trait>::MemberId,
       Balance = BalanceOf<T>,
+      <T as frame_system::Trait>::AccountId,
     {
         MemberRegistered(MemberId),
         MemberProfileUpdated(MemberId),
@@ -321,6 +346,9 @@ decl_event! {
         InitialInvitationBalanceUpdated(Balance),
         LeaderInvitationQuotaUpdated(u32),
         InitialInvitationCountUpdated(u32),
+        StakingAccountAdded(AccountId, MemberId),
+        StakingAccountRemoved(AccountId, MemberId),
+        StakingAccountConfirmed(AccountId, MemberId),
     }
 }
 
@@ -684,6 +712,89 @@ decl_module! {
 
             Self::deposit_event(RawEvent::InitialInvitationCountUpdated(new_invitation_count));
         }
+
+        /// Add staking account candidate for a member.
+        /// The membership must be confirmed before usage.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn add_staking_account_candidate(origin, member_id: T::MemberId) {
+            let staking_account_id = ensure_signed(origin)?;
+
+            ensure!(
+                !Self::staking_account_registered(&staking_account_id),
+                Error::<T>::StakingAccountIsAlreadyRegistered
+            );
+
+            Self::ensure_membership(member_id)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <StakingAccountIdMemberStatus<T>>::insert(
+                staking_account_id.clone(),
+                StakingAccountMemberBinding {
+                    member_id,
+                    confirmed: false,
+                }
+            );
+
+            Self::deposit_event(RawEvent::StakingAccountAdded(staking_account_id, member_id));
+        }
+
+        /// Remove staking account for a member.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn remove_staking_account(origin, member_id: T::MemberId) {
+            let staking_account_id = ensure_signed(origin)?;
+
+            Self::ensure_membership(member_id)?;
+
+            ensure!(
+                Self::staking_account_registered_for_member(&staking_account_id, &member_id),
+                Error::<T>::StakingAccountDoesntExist
+            );
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <StakingAccountIdMemberStatus<T>>::remove(staking_account_id.clone());
+
+            Self::deposit_event(RawEvent::StakingAccountRemoved(staking_account_id, member_id));
+        }
+
+        /// Confirm staking account candidate for a member.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn confirm_staking_account(
+            origin,
+            member_id: T::MemberId,
+            staking_account_id: T::AccountId,
+        ) {
+            Self::ensure_member_controller_account_signed(origin, &member_id)?;
+
+            ensure!(
+                Self::staking_account_registered_for_member(&staking_account_id, &member_id),
+                Error::<T>::StakingAccountDoesntExist
+            );
+
+            ensure!(
+                !Self::staking_account_confirmed(&staking_account_id, &member_id),
+                Error::<T>::StakingAccountAlreadyConfirmed
+            );
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <StakingAccountIdMemberStatus<T>>::insert(
+                staking_account_id.clone(),
+                StakingAccountMemberBinding {
+                    member_id,
+                    confirmed: true,
+                }
+            );
+
+            Self::deposit_event(RawEvent::StakingAccountConfirmed(staking_account_id, member_id));
+        }
     }
 }
 
@@ -857,4 +968,46 @@ impl<T: Trait> Module<T> {
 
         membership_fee.min(referral_cut)
     }
+
+    // Verifies registration of the staking account for ANY member.
+    fn staking_account_registered(staking_account_id: &T::AccountId) -> bool {
+        <StakingAccountIdMemberStatus<T>>::contains_key(staking_account_id)
+    }
+
+    // Verifies registration of the staking account for SOME member.
+    fn staking_account_registered_for_member(
+        staking_account_id: &T::AccountId,
+        member_id: &T::MemberId,
+    ) -> bool {
+        if !Self::staking_account_registered(staking_account_id) {
+            return false;
+        }
+
+        let member_status = Self::staking_account_id_member_status(staking_account_id);
+
+        member_status.member_id == *member_id
+    }
+
+    // Verifies confirmation of the staking account.
+    fn staking_account_confirmed(
+        staking_account_id: &T::AccountId,
+        member_id: &T::MemberId,
+    ) -> bool {
+        if !Self::staking_account_registered_for_member(staking_account_id, member_id) {
+            return false;
+        }
+
+        let member_status = Self::staking_account_id_member_status(staking_account_id);
+
+        member_status.confirmed
+    }
+}
+
+impl<T: Trait> common::StakingAccountValidator<T> for Module<T> {
+    fn is_member_staking_account(
+        member_id: &common::MemberId<T>,
+        account_id: &T::AccountId,
+    ) -> bool {
+        Self::staking_account_confirmed(account_id, member_id)
+    }
 }

+ 118 - 0
runtime-modules/membership/src/tests/fixtures.rs

@@ -553,3 +553,121 @@ impl SetInitialInvitationCountFixture {
         Self { origin, ..self }
     }
 }
+
+pub struct AddStakingAccountFixture {
+    pub origin: RawOrigin<u64>,
+    pub member_id: u64,
+    pub staking_account_id: u64,
+}
+
+impl Default for AddStakingAccountFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(ALICE_ACCOUNT_ID),
+            member_id: ALICE_MEMBER_ID,
+            staking_account_id: ALICE_ACCOUNT_ID,
+        }
+    }
+}
+
+impl AddStakingAccountFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result =
+            Membership::add_staking_account_candidate(self.origin.clone().into(), self.member_id);
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert!(<crate::StakingAccountIdMemberStatus<Test>>::contains_key(
+                &self.staking_account_id,
+            ));
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+}
+
+pub struct RemoveStakingAccountFixture {
+    pub origin: RawOrigin<u64>,
+    pub member_id: u64,
+    pub staking_account_id: u64,
+}
+
+impl Default for RemoveStakingAccountFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(ALICE_ACCOUNT_ID),
+            member_id: ALICE_MEMBER_ID,
+            staking_account_id: ALICE_ACCOUNT_ID,
+        }
+    }
+}
+
+impl RemoveStakingAccountFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result =
+            Membership::remove_staking_account(self.origin.clone().into(), self.member_id);
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert!(!<crate::StakingAccountIdMemberStatus<Test>>::contains_key(
+                &self.staking_account_id,
+            ));
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+}
+
+pub struct ConfirmStakingAccountFixture {
+    pub origin: RawOrigin<u64>,
+    pub member_id: u64,
+    pub staking_account_id: u64,
+}
+
+impl Default for ConfirmStakingAccountFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Signed(ALICE_ACCOUNT_ID),
+            member_id: ALICE_MEMBER_ID,
+            staking_account_id: ALICE_ACCOUNT_ID,
+        }
+    }
+}
+
+impl ConfirmStakingAccountFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Membership::confirm_staking_account(
+            self.origin.clone().into(),
+            self.member_id,
+            self.staking_account_id,
+        );
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert!(<crate::StakingAccountIdMemberStatus<Test>>::get(&ALICE_ACCOUNT_ID,).confirmed);
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+
+    pub fn with_member_id(self, member_id: u64) -> Self {
+        Self { member_id, ..self }
+    }
+}

+ 1 - 0
runtime-modules/membership/src/tests/mock.rs

@@ -110,6 +110,7 @@ impl working_group::Trait<MembershipWorkingGroupInstance> for Test {
     type Event = TestEvent;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = staking_handler::StakingManager<Self, LockId>;
+    type StakingAccountValidator = Membership;
     type MemberOriginValidator = ();
     type MinUnstakingPeriodLimit = ();
     type RewardPeriod = ();

+ 207 - 0
runtime-modules/membership/src/tests/mod.rs

@@ -7,6 +7,7 @@ use crate::{Error, Event};
 use fixtures::*;
 use mock::*;
 
+use common::StakingAccountValidator;
 use frame_support::traits::{LockIdentifier, LockableCurrency, WithdrawReasons};
 use frame_support::{assert_ok, StorageMap, StorageValue};
 use frame_system::RawOrigin;
@@ -734,3 +735,209 @@ fn set_initial_invitation_count_fails_with_invalid_origin() {
             .call_and_assert(Err(DispatchError::BadOrigin));
     });
 }
+
+#[test]
+fn add_staking_account_candidate_succeeds() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        AddStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::StakingAccountAdded(
+            ALICE_ACCOUNT_ID,
+            ALICE_MEMBER_ID,
+        ));
+    });
+}
+
+#[test]
+fn add_staking_account_candidate_fails_with_bad_origin() {
+    build_test_externalities().execute_with(|| {
+        AddStakingAccountFixture::default()
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn add_staking_account_candidate_fails_with_invalid_member_id() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+        let invalid_member_id = 222;
+
+        AddStakingAccountFixture::default()
+            .with_member_id(invalid_member_id)
+            .call_and_assert(Err(Error::<Test>::MemberProfileNotFound.into()));
+    });
+}
+
+#[test]
+fn add_staking_account_candidate_fails_with_duplicated_staking_account_id() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        AddStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        AddStakingAccountFixture::default()
+            .call_and_assert(Err(Error::<Test>::StakingAccountIsAlreadyRegistered.into()));
+    });
+}
+
+#[test]
+fn remove_staking_account_succeeds() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        AddStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        RemoveStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::StakingAccountRemoved(
+            ALICE_ACCOUNT_ID,
+            ALICE_MEMBER_ID,
+        ));
+    });
+}
+
+#[test]
+fn remove_staking_account_fails_with_bad_origin() {
+    build_test_externalities().execute_with(|| {
+        RemoveStakingAccountFixture::default()
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}
+
+#[test]
+fn remove_staking_account_fails_with_invalid_member_id() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+        let invalid_member_id = 222;
+
+        RemoveStakingAccountFixture::default()
+            .with_member_id(invalid_member_id)
+            .call_and_assert(Err(Error::<Test>::MemberProfileNotFound.into()));
+    });
+}
+
+#[test]
+fn remove_staking_account_candidate_fails_with_missing_staking_account_id() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        RemoveStakingAccountFixture::default()
+            .call_and_assert(Err(Error::<Test>::StakingAccountDoesntExist.into()));
+    });
+}
+
+#[test]
+fn confirm_staking_account_succeeds() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        AddStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        ConfirmStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::StakingAccountConfirmed(
+            ALICE_ACCOUNT_ID,
+            ALICE_MEMBER_ID,
+        ));
+    });
+}
+
+#[test]
+fn confirm_staking_account_fails_on_double_confirmation() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        AddStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        ConfirmStakingAccountFixture::default().call_and_assert(Ok(()));
+        ConfirmStakingAccountFixture::default()
+            .call_and_assert(Err(Error::<Test>::StakingAccountAlreadyConfirmed.into()));
+    });
+}
+
+#[test]
+fn confirm_staking_account_fails_with_bad_origin() {
+    build_test_externalities().execute_with(|| {
+        ConfirmStakingAccountFixture::default()
+            .with_origin(RawOrigin::None)
+            .call_and_assert(Err(Error::<Test>::UnsignedOrigin.into()));
+    });
+}
+
+#[test]
+fn confirm_staking_account_fails_with_invalid_member_id() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = DefaultMembershipPrice::get();
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+        let invalid_member_id = 222;
+
+        ConfirmStakingAccountFixture::default()
+            .with_member_id(invalid_member_id)
+            .call_and_assert(Err(Error::<Test>::MemberProfileNotFound.into()));
+    });
+}
+
+#[test]
+fn confirm_staking_account_candidate_fails_with_missing_staking_account_id() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        ConfirmStakingAccountFixture::default()
+            .call_and_assert(Err(Error::<Test>::StakingAccountDoesntExist.into()));
+    });
+}
+
+#[test]
+fn is_member_staking_account_works() {
+    let initial_members = [(ALICE_MEMBER_ID, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        // Before adding candidate should be false.
+        assert_eq!(
+            Membership::is_member_staking_account(&ALICE_MEMBER_ID, &ALICE_ACCOUNT_ID),
+            false
+        );
+        AddStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        // After adding but before confirmation of the candidate should be false.
+        assert_eq!(
+            Membership::is_member_staking_account(&ALICE_MEMBER_ID, &ALICE_ACCOUNT_ID),
+            false
+        );
+        ConfirmStakingAccountFixture::default().call_and_assert(Ok(()));
+
+        // After confirmation of the candidate should be true.
+        assert_eq!(
+            Membership::is_member_staking_account(&ALICE_MEMBER_ID, &ALICE_ACCOUNT_ID),
+            true
+        );
+
+        // After removing of the staking account should be false.
+        RemoveStakingAccountFixture::default().call_and_assert(Ok(()));
+        assert_eq!(
+            Membership::is_member_staking_account(&ALICE_MEMBER_ID, &ALICE_ACCOUNT_ID),
+            false
+        );
+    });
+}

+ 2 - 0
runtime-modules/proposals/codex/src/tests/mock.rs

@@ -232,6 +232,7 @@ impl working_group::Trait<ContentDirectoryWorkingGroupInstance> for Test {
     type Event = ();
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = staking_handler::StakingManager<Self, LockId1>;
+    type StakingAccountValidator = membership::Module<Test>;
     type MemberOriginValidator = ();
     type MinUnstakingPeriodLimit = ();
     type RewardPeriod = ();
@@ -314,6 +315,7 @@ impl working_group::Trait<StorageWorkingGroupInstance> for Test {
     type Event = ();
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = staking_handler::StakingManager<Self, LockId2>;
+    type StakingAccountValidator = membership::Module<Test>;
     type MemberOriginValidator = ();
     type MinUnstakingPeriodLimit = ();
     type RewardPeriod = ();

+ 1 - 0
runtime-modules/service-discovery/src/mock.rs

@@ -145,6 +145,7 @@ impl working_group::Trait<StorageWorkingGroupInstance> for Test {
     type Event = MetaEvent;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = staking_handler::StakingManager<Self, LockId1>;
+    type StakingAccountValidator = membership::Module<Test>;
     type MemberOriginValidator = ();
     type MinUnstakingPeriodLimit = ();
     type RewardPeriod = ();

+ 0 - 1
runtime-modules/staking-handler/Cargo.toml

@@ -18,7 +18,6 @@ sp-core = { package = 'sp-core', default-features = false, git = 'https://github
 codec = { package = 'parity-scale-codec', version = '1.3.1', default-features = false, features = ['derive'] }
 sp-runtime = { package = 'sp-runtime', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
 pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = 'a200cdb93c6af5763b9c7bf313fa708764ac88ca'}
-membership = { package = 'pallet-membership', default-features = false, path = '../membership'}
 
 [features]
 default = ['std']

+ 0 - 9
runtime-modules/staking-handler/src/lib.rs

@@ -6,7 +6,6 @@
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
-use common::MemberId;
 use frame_support::dispatch::{DispatchError, DispatchResult};
 use frame_support::traits::{Currency, Get, LockIdentifier, LockableCurrency, WithdrawReasons};
 use sp_arithmetic::traits::Zero;
@@ -39,9 +38,6 @@ pub trait StakingHandler<T: frame_system::Trait + common::Trait + pallet_balance
     /// Sets the new stake to a given amount.
     fn set_stake(account_id: &T::AccountId, new_stake: BalanceOf<T>) -> DispatchResult;
 
-    /// Verifies that staking account bound to the member.
-    fn is_member_staking_account(member_id: &MemberId<T>, account_id: &T::AccountId) -> bool;
-
     /// Verifies that there no conflicting stakes on the staking account.
     fn is_account_free_of_conflicting_stakes(account_id: &T::AccountId) -> bool;
 
@@ -129,11 +125,6 @@ impl<
         Ok(())
     }
 
-    // Membership support for staking accounts required.
-    fn is_member_staking_account(_member_id: &MemberId<T>, _account_id: &T::AccountId) -> bool {
-        true
-    }
-
     fn is_account_free_of_conflicting_stakes(account_id: &T::AccountId) -> bool {
         let locks = <pallet_balances::Module<T>>::locks(&account_id);
 

+ 1 - 25
runtime-modules/staking-handler/src/mock.rs

@@ -4,17 +4,13 @@ use sp_core::H256;
 use sp_runtime::{
     testing::Header,
     traits::{BlakeTwo256, IdentityLookup},
-    DispatchResult, Perbill,
+    Perbill,
 };
 
 impl_outer_origin! {
     pub enum Origin for Test {}
 }
 
-mod membership_mod {
-    pub use membership::Event;
-}
-
 parameter_types! {
     pub const BlockHashCount: u64 = 250;
     pub const MaximumBlockWeight: u32 = 1024;
@@ -73,26 +69,6 @@ impl common::Trait for Test {
     type ActorId = u64;
 }
 
-impl membership::Trait for Test {
-    type Event = ();
-    type DefaultMembershipPrice = DefaultMembershipPrice;
-    type WorkingGroup = ();
-    type DefaultInitialInvitationBalance = ();
-}
-
-impl common::working_group::WorkingGroupIntegration<Test> for () {
-    fn ensure_worker_origin(
-        _origin: <Test as frame_system::Trait>::Origin,
-        _worker_id: &<Test as common::Trait>::ActorId,
-    ) -> DispatchResult {
-        unimplemented!();
-    }
-
-    fn get_leader_member_id() -> Option<<Test as common::Trait>::MemberId> {
-        unimplemented!();
-    }
-}
-
 impl pallet_timestamp::Trait for Test {
     type Moment = u64;
     type OnTimestampSet = ();

+ 0 - 15
runtime-modules/staking-handler/src/test.rs

@@ -177,21 +177,6 @@ fn is_enough_balance_for_stake_succeeds() {
     });
 }
 
-// Test stub: not implemented yet.
-#[ignore]
-#[test]
-fn is_member_staking_account_succeeds() {
-    build_test_externalities().execute_with(|| {
-        let account_id = 1;
-        let member_id = 1;
-
-        assert!(TestStakingManager::is_member_staking_account(
-            &member_id,
-            &account_id
-        ));
-    });
-}
-
 #[test]
 fn set_stake_succeeds() {
     build_test_externalities().execute_with(|| {

+ 1 - 0
runtime-modules/storage/src/tests/mock.rs

@@ -164,6 +164,7 @@ impl working_group::Trait<StorageWorkingGroupInstance> for Test {
     type Event = MetaEvent;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = staking_handler::StakingManager<Self, LockId>;
+    type StakingAccountValidator = membership::Module<Test>;
     type MemberOriginValidator = ();
     type MinUnstakingPeriodLimit = ();
     type RewardPeriod = ();

+ 8 - 2
runtime-modules/working-group/src/lib.rs

@@ -57,7 +57,7 @@ use types::{ApplicationInfo, WorkerInfo};
 pub use checks::{ensure_origin_is_active_leader, ensure_worker_exists, ensure_worker_signed};
 
 use common::origin::ActorOriginValidator;
-use common::MemberId;
+use common::{MemberId, StakingAccountValidator};
 use frame_support::dispatch::DispatchResult;
 use staking_handler::StakingHandler;
 
@@ -104,6 +104,9 @@ pub trait Trait<I: Instance = DefaultInstance>:
     /// Stakes and balance locks handler.
     type StakingHandler: StakingHandler<Self>;
 
+    /// Validates staking account ownership for a member.
+    type StakingAccountValidator: common::StakingAccountValidator<Self>;
+
     /// Validates member id and origin combination
     type MemberOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
 
@@ -385,7 +388,10 @@ decl_module! {
             // Checks external conditions for staking.
             if let Some(sp) = p.stake_parameters.clone() {
                 ensure!(
-                    T::StakingHandler::is_member_staking_account(&p.member_id, &sp.staking_account_id),
+                    T::StakingAccountValidator::is_member_staking_account(
+                        &p.member_id,
+                        &sp.staking_account_id
+                    ),
                     Error::<T, I>::InvalidStakingAccountForMember
                 );
 

+ 7 - 0
runtime-modules/working-group/src/tests/mock.rs

@@ -118,12 +118,19 @@ impl Trait for Test {
     type Event = TestEvent;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = staking_handler::StakingManager<Self, LockId>;
+    type StakingAccountValidator = ();
     type MemberOriginValidator = ();
     type MinUnstakingPeriodLimit = MinUnstakingPeriodLimit;
     type RewardPeriod = RewardPeriod;
     type WeightInfo = ();
 }
 
+impl common::StakingAccountValidator<Test> for () {
+    fn is_member_staking_account(_: &u64, _: &u64) -> bool {
+        true
+    }
+}
+
 impl crate::WeightInfo for () {
     fn on_initialize_leaving(_: u32) -> Weight {
         0

+ 4 - 0
runtime/src/lib.rs

@@ -633,6 +633,7 @@ impl working_group::Trait<ForumWorkingGroupInstance> for Runtime {
     type Event = Event;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = ForumWorkingGroupStakingManager;
+    type StakingAccountValidator = Members;
     type MemberOriginValidator = MembershipOriginValidator<Self>;
     type MinUnstakingPeriodLimit = MinUnstakingPeriodLimit;
     type RewardPeriod = ForumWorkingGroupRewardPeriod;
@@ -643,6 +644,7 @@ impl working_group::Trait<StorageWorkingGroupInstance> for Runtime {
     type Event = Event;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = StorageWorkingGroupStakingManager;
+    type StakingAccountValidator = Members;
     type MemberOriginValidator = MembershipOriginValidator<Self>;
     type MinUnstakingPeriodLimit = MinUnstakingPeriodLimit;
     type RewardPeriod = StorageWorkingGroupRewardPeriod;
@@ -653,6 +655,7 @@ impl working_group::Trait<ContentDirectoryWorkingGroupInstance> for Runtime {
     type Event = Event;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = ContentDirectoryWorkingGroupStakingManager;
+    type StakingAccountValidator = Members;
     type MemberOriginValidator = MembershipOriginValidator<Self>;
     type MinUnstakingPeriodLimit = MinUnstakingPeriodLimit;
     type RewardPeriod = ContentWorkingGroupRewardPeriod;
@@ -663,6 +666,7 @@ impl working_group::Trait<MembershipWorkingGroupInstance> for Runtime {
     type Event = Event;
     type MaxWorkerNumberLimit = MaxWorkerNumberLimit;
     type StakingHandler = MembershipWorkingGroupStakingManager;
+    type StakingAccountValidator = Members;
     type MemberOriginValidator = MembershipOriginValidator<Self>;
     type MinUnstakingPeriodLimit = MinUnstakingPeriodLimit;
     type RewardPeriod = MembershipRewardPeriod;

+ 16 - 1
runtime/src/tests/proposals_integration/mod.rs

@@ -4,7 +4,7 @@
 
 mod working_group_proposals;
 
-use crate::{BlockNumber, ProposalCancellationFee, Runtime};
+use crate::{BlockNumber, MemberId, ProposalCancellationFee, Runtime};
 use codec::Encode;
 use governance::election_params::ElectionParameters;
 use proposals_codex::{GeneralProposalParameters, ProposalDetails};
@@ -497,6 +497,21 @@ where
     }
 }
 
+pub fn add_confirmed_staking_account(member_id: MemberId, account_id: AccountId32) {
+    assert!(crate::Members::add_staking_account_candidate(
+        RawOrigin::Signed(account_id.clone()).into(),
+        member_id,
+    )
+    .is_ok());
+
+    assert!(crate::Members::confirm_staking_account(
+        RawOrigin::Signed(account_id.clone()).into(),
+        member_id,
+        account_id,
+    )
+    .is_ok());
+}
+
 #[test]
 fn text_proposal_execution_succeeds() {
     initial_test_ext().execute_with(|| {

+ 8 - 0
runtime/src/tests/proposals_integration/working_group_proposals.rs

@@ -535,6 +535,8 @@ fn run_create_decrease_group_leader_stake_proposal_execution_succeeds<
 
         let opening_id = add_opening(member_id, account_id, stake_policy, 1, working_group);
 
+        add_confirmed_staking_account(member_id, account_id.into());
+
         let apply_result = WorkingGroupInstance::<T, I>::apply_on_opening(
             RawOrigin::Signed(account_id.into()).into(),
             working_group::ApplyOnOpeningParameters::<T> {
@@ -660,6 +662,8 @@ fn run_create_slash_group_leader_stake_proposal_execution_succeeds<
 
         let opening_id = add_opening(member_id, account_id, stake_policy, 1, working_group);
 
+        add_confirmed_staking_account(member_id, account_id.into());
+
         let apply_result = WorkingGroupInstance::<T, I>::apply_on_opening(
             RawOrigin::Signed(account_id.into()).into(),
             working_group::ApplyOnOpeningParameters::<T> {
@@ -950,6 +954,8 @@ fn run_create_terminate_group_leader_role_proposal_execution_succeeds<
 
         let opening_id = add_opening(member_id, account_id, stake_policy, 1, working_group);
 
+        add_confirmed_staking_account(member_id, account_id.into());
+
         let apply_result = WorkingGroupInstance::<T, I>::apply_on_opening(
             RawOrigin::Signed(account_id.into()).into(),
             working_group::ApplyOnOpeningParameters::<T> {
@@ -1074,6 +1080,8 @@ fn run_create_terminate_group_leader_role_proposal_with_slashing_execution_succe
 
         let opening_id = add_opening(member_id, account_id, stake_policy, 1, working_group);
 
+        add_confirmed_staking_account(member_id, account_id.into());
+
         let apply_result = WorkingGroupInstance::<T, I>::apply_on_opening(
             RawOrigin::Signed(account_id.into()).into(),
             working_group::ApplyOnOpeningParameters::<T> {