Browse Source

Merge pull request #195 from mnaamani/staking-instant-slashing

Staking: add slash_immediate function
shamil-gadelshin 5 years ago
parent
commit
3f58916f66

+ 1 - 1
runtime-modules/stake/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-stake-module'
-version = '1.0.1'
+version = '2.0.0'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 6 - 0
runtime-modules/stake/src/errors.rs

@@ -44,6 +44,12 @@ pub enum DecreasingStakeError {
     CannotDecreaseStakeWhileUnstaking,
 }
 
+#[derive(Debug, Eq, PartialEq)]
+pub enum ImmediateSlashingError {
+    NotStaked,
+    SlashAmountShouldBeGreaterThanZero,
+}
+
 #[derive(Debug, Eq, PartialEq)]
 pub enum InitiateSlashingError {
     NotStaked,

+ 112 - 17
runtime-modules/stake/src/lib.rs

@@ -69,12 +69,15 @@ pub trait StakingEventsHandler<T: Trait> {
         remaining_imbalance: NegativeImbalance<T>,
     ) -> NegativeImbalance<T>;
 
-    // Handler for slashing event.
-    // NB: actually_slashed can be less than amount of the slash itself if the
-    // claim amount on the stake cannot cover it fully.
+    /// Handler for slashing event.
+    /// NB: actually_slashed can be less than amount of the slash itself if the
+    /// claim amount on the stake cannot cover it fully.
+    /// The SlashId is optional, as slashing may not be associated with a slashing that was initiated, but was an immediate slashing.
+    /// For Immediate slashes, the stake may have transitioned to NotStaked so handler should not assume the state
+    /// is still in staked status.
     fn slashed(
         id: &T::StakeId,
-        slash_id: &T::SlashId,
+        slash_id: Option<T::SlashId>,
         slashed_amount: BalanceOf<T>,
         remaining_stake: BalanceOf<T>,
         remaining_imbalance: NegativeImbalance<T>,
@@ -93,7 +96,7 @@ impl<T: Trait> StakingEventsHandler<T> for () {
 
     fn slashed(
         _id: &T::StakeId,
-        _slash_id: &T::SlashId,
+        _slash_id: Option<T::SlashId>,
         _slahed_amount: BalanceOf<T>,
         _remaining_stake: BalanceOf<T>,
         _remaining_imbalance: NegativeImbalance<T>,
@@ -121,7 +124,7 @@ impl<T: Trait, X: StakingEventsHandler<T>, Y: StakingEventsHandler<T>> StakingEv
 
     fn slashed(
         id: &T::StakeId,
-        slash_id: &T::SlashId,
+        slash_id: Option<T::SlashId>,
         slashed_amount: BalanceOf<T>,
         remaining_stake: BalanceOf<T>,
         imbalance: NegativeImbalance<T>,
@@ -243,16 +246,12 @@ where
 
     /// Executes a Slash. If remaining at stake drops below the minimum_balance, it will slash the entire staked amount.
     /// Returns the actual slashed amount.
-    fn apply_slash(
-        &mut self,
-        slash: Slash<BlockNumber, Balance>,
-        minimum_balance: Balance,
-    ) -> Balance {
+    fn apply_slash(&mut self, slash_amount: Balance, minimum_balance: Balance) -> Balance {
         // calculate how much to slash
-        let mut slash_amount = if slash.slash_amount > self.staked_amount {
+        let mut slash_amount = if slash_amount > self.staked_amount {
             self.staked_amount
         } else {
-            slash.slash_amount
+            slash_amount
         };
 
         // apply the slashing
@@ -274,7 +273,7 @@ where
 
         for (slash_id, slash) in self.get_slashes_to_finalize().iter() {
             // apply the slashing and get back actual amount slashed
-            let slashed_amount = self.apply_slash(*slash, minimum_balance);
+            let slashed_amount = self.apply_slash(slash.slash_amount, minimum_balance);
 
             finalized_slashes.push((*slash_id, slashed_amount, self.staked_amount));
         }
@@ -307,7 +306,7 @@ pub struct Stake<BlockNumber, Balance, SlashId: Ord> {
 
 impl<BlockNumber, Balance, SlashId> Stake<BlockNumber, Balance, SlashId>
 where
-    BlockNumber: Copy + SimpleArithmetic,
+    BlockNumber: Copy + SimpleArithmetic + Zero,
     Balance: Copy + SimpleArithmetic,
     SlashId: Copy + Ord + Zero + One,
 {
@@ -408,6 +407,31 @@ where
         }
     }
 
+    fn slash_immediate(
+        &mut self,
+        slash_amount: Balance,
+        minimum_balance: Balance,
+    ) -> Result<(Balance, Balance), ImmediateSlashingError> {
+        ensure!(
+            slash_amount > Zero::zero(),
+            ImmediateSlashingError::SlashAmountShouldBeGreaterThanZero
+        );
+
+        match self.staking_status {
+            StakingStatus::Staked(ref mut staked_state) => {
+                // irrespective of wether we are unstaking or not, slash!
+
+                let actually_slashed = staked_state.apply_slash(slash_amount, minimum_balance);
+
+                let remaining_stake = staked_state.staked_amount;
+
+                Ok((actually_slashed, remaining_stake))
+            }
+            // can't slash if not staked
+            _ => Err(ImmediateSlashingError::NotStaked),
+        }
+    }
+
     fn initiate_slashing(
         &mut self,
         slash_amount: Balance,
@@ -680,6 +704,14 @@ where
     }
 }
 
+#[derive(Debug, Eq, PartialEq)]
+pub struct SlashImmediateOutcome<Balance, NegativeImbalance> {
+    pub caused_unstake: bool,
+    pub actually_slashed: Balance,
+    pub remaining_stake: Balance,
+    pub remaining_imbalance: NegativeImbalance,
+}
+
 decl_storage! {
     trait Store for Module<T: Trait> as StakePool {
         /// Maps identifiers to a stake.
@@ -938,7 +970,70 @@ impl<T: Trait> Module<T> {
         Ok(staked_amount)
     }
 
-    /// Initiate a new slashing of a staked stake.
+    /// Slashes a stake with immediate effect, returns the outcome of the slashing.
+    /// Can optionally specify if slashing can result in immediate unstaking if staked amount
+    /// after slashing goes to zero.
+    pub fn slash_immediate(
+        stake_id: &T::StakeId,
+        slash_amount: BalanceOf<T>,
+        unstake_on_zero_staked: bool,
+    ) -> Result<
+        SlashImmediateOutcome<BalanceOf<T>, NegativeImbalance<T>>,
+        StakeActionError<ImmediateSlashingError>,
+    > {
+        let mut stake = ensure_stake_exists!(T, stake_id, StakeActionError::StakeNotFound)?;
+
+        // Get amount at stake before slashing to be used in unstaked event trigger
+        let staked_amount_before_slash = ensure_staked_amount!(
+            stake,
+            StakeActionError::Error(ImmediateSlashingError::NotStaked)
+        )?;
+
+        let (actually_slashed, remaining_stake) =
+            stake.slash_immediate(slash_amount, T::Currency::minimum_balance())?;
+
+        let caused_unstake = unstake_on_zero_staked && remaining_stake == BalanceOf::<T>::zero();
+
+        if caused_unstake {
+            stake.staking_status = StakingStatus::NotStaked;
+        }
+
+        // Update state before calling handlers!
+        <Stakes<T>>::insert(stake_id, stake);
+
+        // Remove the slashed amount from the pool
+        let slashed_imbalance = Self::withdraw_funds_from_stake_pool(actually_slashed);
+
+        // Notify slashing event handler before unstaked handler.
+        let remaining_imbalance_after_slash_handler = T::StakingEventsHandler::slashed(
+            stake_id,
+            None,
+            actually_slashed,
+            remaining_stake,
+            slashed_imbalance,
+        );
+
+        let remaining_imbalance = if caused_unstake {
+            // Notify unstaked handler with any remaining unused imbalance
+            // from the slashing event handler
+            T::StakingEventsHandler::unstaked(
+                &stake_id,
+                staked_amount_before_slash,
+                remaining_imbalance_after_slash_handler,
+            )
+        } else {
+            remaining_imbalance_after_slash_handler
+        };
+
+        Ok(SlashImmediateOutcome {
+            caused_unstake,
+            actually_slashed,
+            remaining_stake,
+            remaining_imbalance,
+        })
+    }
+
+    /// Initiate a new slashing of a staked stake. Slashing begins at next block.
     pub fn initiate_slashing(
         stake_id: &T::StakeId,
         slash_amount: BalanceOf<T>,
@@ -1065,7 +1160,7 @@ impl<T: Trait> Module<T> {
 
                 let _ = T::StakingEventsHandler::slashed(
                     &stake_id,
-                    &slash_id,
+                    Some(slash_id),
                     slashed_amount,
                     staked_amount,
                     imbalance,

+ 10 - 0
runtime-modules/stake/src/macroes.rs

@@ -17,3 +17,13 @@ macro_rules! ensure_stake_exists {
         ensure_map_has_mapping_with_key!(Stakes, $runtime_trait, $stake_id, $error)
     }};
 }
+
+#[macro_export]
+macro_rules! ensure_staked_amount {
+    ($stake:expr, $error:expr) => {{
+        match $stake.staking_status {
+            StakingStatus::Staked(ref staked_state) => Ok(staked_state.staked_amount),
+            _ => Err($error),
+        }
+    }};
+}

+ 175 - 0
runtime-modules/stake/src/tests.rs

@@ -802,3 +802,178 @@ fn unstake() {
         assert_eq!(StakePool::stake_pool_balance(), starting_stake_fund_balance);
     });
 }
+
+#[test]
+fn immediate_slashing_cannot_slash_non_existent_stake() {
+    build_test_externalities().execute_with(|| {
+        let outcome = StakePool::slash_immediate(&100, 5000, false);
+        assert!(outcome.is_err());
+        let error = outcome.err().unwrap();
+        assert_eq!(error, StakeActionError::StakeNotFound);
+    });
+}
+
+#[test]
+fn immediate_slashing_without_unstaking() {
+    build_test_externalities().execute_with(|| {
+        const UNSTAKE_POLICY: bool = false;
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+
+        let stake_id = StakePool::create_stake();
+        let created_at = System::block_number();
+        let initial_stake_state = Stake {
+            created: created_at,
+            staking_status: StakingStatus::Staked(StakedState {
+                staked_amount,
+                staked_status: StakedStatus::Normal,
+                next_slash_id: 0,
+                ongoing_slashes: BTreeMap::new(),
+            }),
+        };
+        <Stakes<Test>>::insert(&stake_id, initial_stake_state);
+
+        let slash_amount = 5000;
+
+        let outcome = StakePool::slash_immediate(&stake_id, slash_amount, UNSTAKE_POLICY);
+        assert!(outcome.is_ok());
+        let outcome = outcome.ok().unwrap();
+
+        assert_eq!(outcome.caused_unstake, false);
+        assert_eq!(outcome.actually_slashed, slash_amount);
+        assert_eq!(outcome.remaining_stake, staked_amount - slash_amount);
+        // Default handler destroys imbalance
+        assert_eq!(outcome.remaining_imbalance.peek(), 0);
+
+        assert_eq!(
+            <Stakes<Test>>::get(stake_id),
+            Stake {
+                created: created_at,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: outcome.remaining_stake,
+                    staked_status: StakedStatus::Normal,
+                    next_slash_id: 0,
+                    ongoing_slashes: BTreeMap::new()
+                }),
+            }
+        );
+
+        // slash to zero but without asking to unstake
+        // Slash and unstake by making slash go to zero
+        let slash_amount = outcome.remaining_stake;
+        let outcome = StakePool::slash_immediate(&stake_id, slash_amount, UNSTAKE_POLICY)
+            .ok()
+            .unwrap();
+        assert_eq!(outcome.caused_unstake, false);
+        assert_eq!(outcome.actually_slashed, slash_amount);
+        assert_eq!(outcome.remaining_stake, 0);
+        // Default handler destroys imbalance
+        assert_eq!(outcome.remaining_imbalance.peek(), 0);
+
+        // Should still be staked, even if staked amount = 0
+        assert_eq!(
+            <Stakes<Test>>::get(stake_id),
+            Stake {
+                created: created_at,
+                staking_status: StakingStatus::Staked(StakedState {
+                    staked_amount: 0,
+                    staked_status: StakedStatus::Normal,
+                    next_slash_id: 0,
+                    ongoing_slashes: BTreeMap::new()
+                }),
+            }
+        );
+    });
+}
+
+#[test]
+fn immediate_slashing_with_unstaking() {
+    build_test_externalities().execute_with(|| {
+        const UNSTAKE_POLICY: bool = true;
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+
+        let stake_id = StakePool::create_stake();
+        let created_at = System::block_number();
+        let initial_stake_state = Stake {
+            created: created_at,
+            staking_status: StakingStatus::Staked(StakedState {
+                staked_amount,
+                staked_status: StakedStatus::Normal,
+                next_slash_id: 0,
+                ongoing_slashes: BTreeMap::new(),
+            }),
+        };
+        <Stakes<Test>>::insert(&stake_id, initial_stake_state);
+
+        // Slash whole amount unstake by making slash go to zero
+        let slash_amount = staked_amount;
+        let outcome = StakePool::slash_immediate(&stake_id, slash_amount, UNSTAKE_POLICY)
+            .ok()
+            .unwrap();
+        assert_eq!(outcome.caused_unstake, true);
+        assert_eq!(outcome.actually_slashed, slash_amount);
+        assert_eq!(outcome.remaining_stake, 0);
+        // Default handler destroys imbalance
+        assert_eq!(outcome.remaining_imbalance.peek(), 0);
+        // Should now be unstaked
+        assert_eq!(
+            <Stakes<Test>>::get(stake_id),
+            Stake {
+                created: created_at,
+                staking_status: StakingStatus::NotStaked
+            }
+        );
+    });
+}
+
+#[test]
+fn immediate_slashing_cannot_slash_if_not_staked() {
+    build_test_externalities().execute_with(|| {
+        let stake_id = StakePool::create_stake();
+        let created_at = System::block_number();
+        let initial_stake_state = Stake {
+            created: created_at,
+            staking_status: StakingStatus::NotStaked,
+        };
+        <Stakes<Test>>::insert(&stake_id, initial_stake_state);
+
+        let outcome = StakePool::slash_immediate(&stake_id, 1, false);
+        let outcome_err = outcome.err().unwrap();
+        assert_eq!(
+            outcome_err,
+            StakeActionError::Error(ImmediateSlashingError::NotStaked)
+        );
+    });
+}
+
+#[test]
+fn immediate_slashing_cannot_slash_zero() {
+    build_test_externalities().execute_with(|| {
+        let staked_amount = Balances::minimum_balance() + 10000;
+        let _ = Balances::deposit_creating(&StakePool::stake_pool_account_id(), staked_amount);
+
+        let stake_id = StakePool::create_stake();
+        let created_at = System::block_number();
+        let initial_stake_state = Stake {
+            created: created_at,
+            staking_status: StakingStatus::Staked(StakedState {
+                staked_amount,
+                staked_status: StakedStatus::Normal,
+                next_slash_id: 0,
+                ongoing_slashes: BTreeMap::new(),
+            }),
+        };
+        <Stakes<Test>>::insert(&stake_id, initial_stake_state);
+
+        const ZERO_SLASH_AMOUNT: u64 = 0;
+
+        let outcome_err = StakePool::slash_immediate(&stake_id, ZERO_SLASH_AMOUNT, true)
+            .err()
+            .unwrap();
+        assert_eq!(
+            outcome_err,
+            StakeActionError::Error(ImmediateSlashingError::SlashAmountShouldBeGreaterThanZero)
+        );
+    });
+}

+ 1 - 1
runtime/Cargo.toml

@@ -5,7 +5,7 @@ edition = '2018'
 name = 'joystream-node-runtime'
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
 # {Authoring}.{Spec}.{Impl} of the RuntimeVersion
-version = '6.8.0'
+version = '6.8.1'
 
 [features]
 default = ['std']

+ 1 - 1
runtime/src/lib.rs

@@ -632,7 +632,7 @@ impl stake::StakingEventsHandler<Runtime> for ContentWorkingGroupStakingEventHan
     // Handler for slashing event
     fn slashed(
         _id: &<Runtime as stake::Trait>::StakeId,
-        _slash_id: &<Runtime as stake::Trait>::SlashId,
+        _slash_id: Option<<Runtime as stake::Trait>::SlashId>,
         _slashed_amount: stake::BalanceOf<Runtime>,
         _remaining_stake: stake::BalanceOf<Runtime>,
         remaining_imbalance: stake::NegativeImbalance<Runtime>,