Browse Source

Add stake refunding to the working group module.

Shamil Gadelshin 4 years ago
parent
commit
d638525fa4

+ 62 - 23
runtime-modules/working-group/src/lib.rs

@@ -56,8 +56,8 @@ use rstd::collections::btree_set::BTreeSet;
 use rstd::prelude::*;
 use rstd::vec::Vec;
 use sr_primitives::traits::{Bounded, One, Zero};
-use srml_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
-use srml_support::{decl_event, decl_module, decl_storage, ensure};
+use srml_support::traits::{Currency, ExistenceRequirement, Imbalance, WithdrawReasons};
+use srml_support::{decl_event, decl_module, decl_storage, ensure, print};
 use system::{ensure_root, ensure_signed};
 
 use crate::types::ExitInitiationOrigin;
@@ -109,6 +109,9 @@ pub type ApplicationIdToWorkerIdMap<T> = BTreeMap<ApplicationId<T>, WorkerId<T>>
 /// Type identifier for worker role, which must be same as membership actor identifier
 pub type WorkerId<T> = <T as membership::members::Trait>::ActorId;
 
+/// Alias for the application id from the hiring module.
+pub type HiringApplicationId<T> = <T as hiring::Trait>::ApplicationId;
+
 // Type simplification
 type OpeningInfo<T> = (
     Opening<
@@ -117,21 +120,12 @@ type OpeningInfo<T> = (
         BalanceOf<T>,
         ApplicationId<T>,
     >,
-    hiring::Opening<
-        BalanceOf<T>,
-        <T as system::Trait>::BlockNumber,
-        <T as hiring::Trait>::ApplicationId,
-    >,
+    hiring::Opening<BalanceOf<T>, <T as system::Trait>::BlockNumber, HiringApplicationId<T>>,
 );
 
 // Type simplification
 type ApplicationInfo<T> = (
-    Application<
-        <T as system::Trait>::AccountId,
-        OpeningId<T>,
-        MemberId<T>,
-        <T as hiring::Trait>::ApplicationId,
-    >,
+    Application<<T as system::Trait>::AccountId, OpeningId<T>, MemberId<T>, HiringApplicationId<T>>,
     ApplicationId<T>,
     Opening<
         <T as hiring::Trait>::OpeningId,
@@ -320,6 +314,11 @@ decl_storage! {
 
         /// Worker exit rationale text length limits.
         pub WorkerExitRationaleText get(worker_exit_rationale_text) : InputValidationLengthConstraint;
+
+        /// Map member id by hiring application id.
+        /// Required by StakingEventsHandler callback call to refund the balance on unstaking.
+        pub MemberIdByHiringApplicationId get(fn member_id_by_hiring_application_id):
+            map HiringApplicationId<T> =>  MemberId<T>;
     }
         add_extra_genesis {
         config(phantom): rstd::marker::PhantomData<I>;
@@ -628,23 +627,25 @@ decl_module! {
             let opt_application_stake_imbalance = Self::make_stake_opt_imbalance(&opt_application_stake_balance, &source_account);
 
             // Call hiring module to add application
-            let add_application_result = hiring::Module::<T>::add_application(
-                opening.opening_id,
-                opt_role_stake_imbalance,
-                opt_application_stake_imbalance,
-                human_readable_text
-            );
+            let add_application = ensure_on_wrapped_error!(
+                    hiring::Module::<T>::add_application(
+                    opening.opening_id,
+                    opt_role_stake_imbalance,
+                    opt_application_stake_imbalance,
+                    human_readable_text
+                )
+            )?;
 
-            // Has to hold
-            assert!(add_application_result.is_ok());
+            let hiring_application_id = add_application.application_id_added;
 
-            let application_id = add_application_result.unwrap().application_id_added;
+            // Save member id to refund the stakes. This piece of date should outlive the 'worker'.
+            <MemberIdByHiringApplicationId<T, I>>::insert(hiring_application_id, member_id);
 
             // Get id of new worker/lead application
             let new_application_id = NextApplicationId::<T, I>::get();
 
             // Make worker/lead application
-            let application = Application::new(&role_account, &opening_id, &member_id, &application_id);
+            let application = Application::new(&role_account, &opening_id, &member_id, &hiring_application_id);
 
             // Store application
             ApplicationById::<T, I>::insert(new_application_id, application);
@@ -1250,6 +1251,44 @@ pub fn default_text_constraint() -> InputValidationLengthConstraint {
 }
 
 impl<T: Trait<I>, I: Instance> Module<T, I> {
+    /// Callback from StakingEventsHandler. Refunds unstaked imbalance back to the source account.
+    pub fn refund_working_group_stake(
+        stake_id: StakeId<T>,
+        imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        if !hiring::ApplicationIdByStakingId::<T>::exists(stake_id) {
+            print("Working group broken invariant: no stake id in the hiring module.");
+            return imbalance;
+        }
+
+        let hiring_application_id = hiring::ApplicationIdByStakingId::<T>::get(stake_id);
+
+        if !MemberIdByHiringApplicationId::<T, I>::exists(hiring_application_id) {
+            // Stake is not related to the working group module.
+            return imbalance;
+        }
+
+        let member_id = Module::<T, I>::member_id_by_hiring_application_id(hiring_application_id);
+
+        if let Some(member_profile) = membership::members::MemberProfile::<T>::get(member_id) {
+            let refunding_result = CurrencyOf::<T>::resolve_into_existing(
+                &member_profile.controller_account,
+                imbalance,
+            );
+
+            if refunding_result.is_err() {
+                print("Working group broken invariant: cannot refund.");
+                // cannot return imbalance here, because of possible double spending.
+                return <NegativeImbalance<T>>::zero();
+            }
+        } else {
+            print("Working group broken invariant: no member profile.");
+            return imbalance;
+        }
+
+        <NegativeImbalance<T>>::zero()
+    }
+
     /// Checks that provided lead account id belongs to the current working group leader
     pub fn ensure_is_lead_account(lead_account_id: T::AccountId) -> Result<(), Error> {
         let lead = <CurrentLead<T, I>>::get();

+ 50 - 3
runtime-modules/working-group/src/tests/mock.rs

@@ -1,4 +1,4 @@
-use crate::{Module, Trait};
+use crate::{BalanceOf, Module, NegativeImbalance, Trait};
 use common::constraints::InputValidationLengthConstraint;
 use primitives::H256;
 use sr_primitives::{
@@ -6,7 +6,10 @@ use sr_primitives::{
     traits::{BlakeTwo256, IdentityLookup},
     Perbill,
 };
-use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+use srml_support::{
+    impl_outer_event, impl_outer_origin, parameter_types, StorageLinkedMap, StorageMap,
+};
+use std::marker::PhantomData;
 
 impl_outer_origin! {
         pub enum Origin for Test {}
@@ -79,7 +82,7 @@ impl minting::Trait for Test {
 impl stake::Trait for Test {
     type Currency = Balances;
     type StakePoolId = StakePoolId;
-    type StakingEventsHandler = ();
+    type StakingEventsHandler = StakingEventsHandler<Test>;
     type StakeId = u64;
     type SlashId = u64;
 }
@@ -163,3 +166,47 @@ pub fn build_test_externalities() -> runtime_io::TestExternalities {
 
     t.into()
 }
+
+pub struct StakingEventsHandler<T> {
+    pub marker: PhantomData<T>,
+}
+
+impl<T: stake::Trait + crate::Trait<TestWorkingGroupInstance>> stake::StakingEventsHandler<T>
+    for StakingEventsHandler<T>
+{
+    /// Unstake remaining sum back to the source_account_id
+    fn unstaked(
+        stake_id: &<T as stake::Trait>::StakeId,
+        _unstaked_amount: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        // Stake not related to a staked role managed by the hiring module.
+        if !hiring::ApplicationIdByStakingId::<T>::exists(*stake_id) {
+            return remaining_imbalance;
+        }
+
+        let hiring_application_id = hiring::ApplicationIdByStakingId::<T>::get(*stake_id);
+
+        if crate::MemberIdByHiringApplicationId::<T, TestWorkingGroupInstance>::exists(
+            hiring_application_id,
+        ) {
+            return <crate::Module<T, TestWorkingGroupInstance>>::refund_working_group_stake(
+                *stake_id,
+                remaining_imbalance,
+            );
+        }
+
+        remaining_imbalance
+    }
+
+    /// Empty handler for slashing
+    fn slashed(
+        _: &<T as stake::Trait>::StakeId,
+        _: Option<<T as stake::Trait>::SlashId>,
+        _: BalanceOf<T>,
+        _: BalanceOf<T>,
+        remaining_imbalance: NegativeImbalance<T>,
+    ) -> NegativeImbalance<T> {
+        remaining_imbalance
+    }
+}

+ 15 - 0
runtime-modules/working-group/src/tests/mod.rs

@@ -1187,6 +1187,11 @@ fn terminate_worker_role_succeeds_with_stakes() {
             total_balance - stake_balance
         );
 
+        let stake_id = 0;
+        let old_stake = <stake::Module<Test>>::stakes(stake_id);
+
+        assert_eq!(get_stake_balance(old_stake), stake_balance);
+
         let terminate_worker_role_fixture =
             TerminateWorkerRoleFixture::default_for_worker_id(worker_id);
 
@@ -1196,6 +1201,16 @@ fn terminate_worker_role_succeeds_with_stakes() {
             worker_id,
             b"rationale_text".to_vec(),
         ));
+
+        // Balance was restored.
+
+        assert_eq!(get_balance(worker_account_id), total_balance);
+
+        let new_stake = <stake::Module<Test>>::stakes(stake_id);
+        assert!(matches!(
+            new_stake.staking_status,
+            stake::StakingStatus::NotStaked
+        ));
     });
 }