Repository: crustio/crust (rust)

Issue:
[GPoS] DSM's staking pot should be rewarded to candidates & guarantors in 15 days

Pull request:
[GPoS + DSM] New storage staking pot reward mechanism
Fix #382

Patch:
diff --git a/cstrml/market/src/lib.rs b/cstrml/market/src/lib.rs
index 50735cc..903bc50 100644
--- a/cstrml/market/src/lib.rs
+++ b/cstrml/market/src/lib.rs
@@ -10,7 +10,9 @@ use frame_support::{
     dispatch::DispatchResult, ensure,
     storage::migration::remove_storage_prefix,
     traits::{
-        Currency, ReservableCurrency, Get, ExistenceRequirement::AllowDeath,
+        Currency, ReservableCurrency, Get,
+        ExistenceRequirement::{AllowDeath, KeepAlive},
+        WithdrawReasons, Imbalance
     }
 };
 use sp_std::{prelude::*, convert::TryInto, collections::btree_set::BTreeSet};
@@ -33,10 +35,14 @@ mod tests;
 pub mod benchmarking;
 
 use primitives::{
-    MerkleRoot, BlockNumber,
-    traits::{TransferrableCurrency, MarketInterface, SworkerInterface}, SworkerAnchor
+    MerkleRoot, BlockNumber, SworkerAnchor,
+    traits::{
+        TransferrableCurrency, MarketInterface,
+        SworkerInterface
+    }
 };
 
+pub(crate) const LOG_TARGET: &'static str = "market";
 
 #[macro_export]
 macro_rules! log {
@@ -102,8 +108,10 @@ pub struct MerchantLedger<Balance> {
 
 type BalanceOf<T> =
     <<T as Config>::Currency as Currency<<T as system::Config>::AccountId>>::Balance;
+type PositiveImbalanceOf<T> =
+    <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::PositiveImbalance;
 
-impl<T: Config> MarketInterface<<T as system::Config>::AccountId> for Module<T>
+impl<T: Config> MarketInterface<<T as system::Config>::AccountId, BalanceOf<T>> for Module<T>
 {
     /// Upsert new replica
     /// Accept id(who, anchor), cid, valid_at and maybe_member
@@ -187,6 +195,29 @@ impl<T: Config> MarketInterface<<T as system::Config>::AccountId> for Module<T>
         // 3. Delete anchor from file_info/file_trash and return whether it is counted
         Self::delete_used_anchor(cid, anchor)
     }
+
+    // withdraw market staking pot for distributing staking reward
+    fn withdraw_staking_pot() -> BalanceOf<T> {
+        let staking_pot = Self::staking_pot();
+        // Leave the minimum balance to keep this account live.
+        let staking_amount = T::Currency::free_balance(&staking_pot) - T::Currency::minimum_balance();
+        let mut imbalance = <PositiveImbalanceOf<T>>::zero();
+        imbalance.subsume(T::Currency::burn(staking_amount.clone()));
+        if let Err(_) = T::Currency::settle(
+            &staking_pot,
+            imbalance,
+            WithdrawReasons::TRANSFER,
+            KeepAlive
+        ) {
+            log!(
+                warn,
+                "🏢 Something wrong during withdrawing staking pot. This should never happen!"
+            );
+
+            return Zero::zero();
+        }
+        staking_amount
+    }
 }
 
 /// The module's configuration trait.
@@ -630,7 +661,7 @@ impl<T: Config> Module<T> {
                 })
             }
             // trash I is full => dump trash II
-            if Self::used_trash_size_i() == T::UsedTrashMaxSize::get() {
+            if Self::used_trash_size_i() >= T::UsedTrashMaxSize::get() {
                 Self::dump_used_trash_ii();
             }
         } else {
@@ -643,7 +674,7 @@ impl<T: Config> Module<T> {
                 })
             }
             // trash II is full => dump trash I
-            if Self::used_trash_size_ii() == T::UsedTrashMaxSize::get() {
+            if Self::used_trash_size_ii() >= T::UsedTrashMaxSize::get() {
                 Self::dump_used_trash_i();
             }
         }
diff --git a/cstrml/staking/src/lib.rs b/cstrml/staking/src/lib.rs
index 312a3e6..38c2960 100644
--- a/cstrml/staking/src/lib.rs
+++ b/cstrml/staking/src/lib.rs
@@ -22,7 +22,7 @@ use frame_support::{
     weights::{Weight, constants::{WEIGHT_PER_MICROS, WEIGHT_PER_NANOS}},
     traits::{
         Currency, LockIdentifier, LockableCurrency, WithdrawReasons, OnUnbalanced, Imbalance, Get,
-        UnixTime, EnsureOrigin, Randomness, ExistenceRequirement::KeepAlive
+        UnixTime, EnsureOrigin, Randomness, ExistenceRequirement::{KeepAlive, AllowDeath}
     },
     dispatch::DispatchResult
 };
@@ -51,7 +51,7 @@ pub mod weight;
 use swork;
 use primitives::{
     constants::{currency::*, time::*},
-    traits::TransferrableCurrency
+    traits::{TransferrableCurrency, MarketInterface}
 };
 
 const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4;
@@ -435,6 +435,12 @@ pub trait Config: frame_system::Config {
     /// Storage power ratio for crust network phase 1
     type SPowerRatio: Get<u128>;
 
+    /// Reference to Market staking pot.
+    type MarketStakingPot: MarketInterface<Self::AccountId, BalanceOf<Self>>;
+
+    /// Market Staking Pot Duration. Count of EraIndex
+    type MarketStakingPotDuration: Get<u32>;
+
     /// Weight information for extrinsics in this pallet.
     type WeightInfo: WeightInfo;
 }
@@ -529,6 +535,10 @@ decl_storage! {
         pub ErasStakingPayout get(fn eras_staking_payout):
             map hasher(twox_64_concat) EraIndex => Option<BalanceOf<T>>;
 
+        /// Market staking payout of validator at era.
+        pub ErasMarketStakingPayout get(fn eras_market_staking_payout):
+            map hasher(twox_64_concat) EraIndex => Option<BalanceOf<T>>;
+
         /// Authoring payout of validator at era.
         pub ErasAuthoringPayout get(fn eras_authoring_payout):
             double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId
@@ -763,6 +773,9 @@ decl_module! {
         /// The staking's module id, used for deriving its sovereign account ID.
         const ModuleId: ModuleId = T::ModuleId::get();
 
+        /// Total era duration for once dsm staking pot.
+        const MarketStakingPotDuration: u32 = T::MarketStakingPotDuration::get();
+
         type Error = Error<T>;
 
         fn deposit_event() = default;
@@ -1646,6 +1659,9 @@ impl<T: Config> Module<T> {
         // `ledger.claimed_rewards` in this case.
         let era_staking_payout = <ErasStakingPayout<T>>::get(&era)
             .ok_or_else(|| Error::<T>::InvalidEraToReward)?;
+        let era_market_staking_payout = <ErasMarketStakingPayout<T>>::get(&era)
+            .unwrap_or(Zero::zero());
+        let total_era_staking_payout = era_staking_payout.saturating_add(era_market_staking_payout);
 
         let controller = Self::bonded(&validator_stash).ok_or(Error::<T>::NotStash)?;
         let mut ledger = <Ledger<T>>::get(&controller).ok_or_else(|| Error::<T>::NotController)?;
@@ -1670,7 +1686,7 @@ impl<T: Config> Module<T> {
 
         // 3. Retrieve total stakes and total staking reward
         let era_total_stakes = <ErasTotalStakes<T>>::get(&era);
-        let staking_reward = Perbill::from_rational_approximation(to_num(exposure.total), to_num(era_total_stakes)) * era_staking_payout;
+        let staking_reward = Perbill::from_rational_approximation(to_num(exposure.total), to_num(era_total_stakes)) * total_era_staking_payout;
         let total = exposure.total.max(One::one());
         // 4. Calculate total rewards for staking
         let total_rewards = <ErasValidatorPrefs<T>>::get(&era, &ledger.stash).fee * staking_reward;
@@ -1846,8 +1862,8 @@ impl<T: Config> Module<T> {
                 let active_era_index = active_era.index.clone();
                 let points = <ErasRewardPoints<T>>::get(&active_era_index);
                 let mut imbalance = <PositiveImbalanceOf<T>>::zero();
-                let total_authoring_payout = Self::authoring_rewards_in_era();
-                let total_staking_payout = Self::staking_rewards_in_era(active_era_index);
+                let mut total_authoring_payout = Self::authoring_rewards_in_era();
+                let mut total_staking_payout = Self::staking_rewards_in_era(active_era_index);
 
                 // 1. Check whether staking pot has enough money
                 imbalance.subsume(T::Currency::burn(total_authoring_payout.clone()));
@@ -1856,13 +1872,15 @@ impl<T: Config> Module<T> {
                     &staking_pot,
                     imbalance,
                     WithdrawReasons::TRANSFER,
-                    KeepAlive
+                    AllowDeath
                 ) {
                     log!(
                         info,
                         "💸 Staking pot is not enough"
                     );
-                    return;
+                    // Market staking pot won't be skipped.
+                    total_authoring_payout = Zero::zero();
+                    total_staking_payout = Zero::zero();
                 }
                 // 2. Block authoring payout
                 for (v, p) in points.individual.iter() {
@@ -1875,8 +1893,11 @@ impl<T: Config> Module<T> {
     
                 // 3. Staking payout
                 <ErasStakingPayout<T>>::insert(active_era_index, total_staking_payout);
+
+                // 4. Market's staking payout
+                Self::distribute_market_staking_payout(active_era_index);
     
-                // 4. Deposit era reward event
+                // 5. Deposit era reward event
                 Self::deposit_event(RawEvent::EraReward(active_era_index, total_authoring_payout, total_staking_payout));
     
                 // TODO: enable treasury and might bring this back
@@ -1893,6 +1914,7 @@ impl<T: Config> Module<T> {
         <ErasStakersClipped<T>>::remove_prefix(era_index);
         <ErasValidatorPrefs<T>>::remove_prefix(era_index);
         <ErasStakingPayout<T>>::remove(era_index);
+        <ErasMarketStakingPayout<T>>::remove(era_index);
         <ErasTotalStakes<T>>::remove(era_index);
         <ErasAuthoringPayout<T>>::remove_prefix(era_index);
         <ErasRewardPoints<T>>::remove(era_index);
@@ -1939,6 +1961,19 @@ impl<T: Config> Module<T> {
         reward_this_era.try_into().ok().unwrap()
     }
 
+    fn distribute_market_staking_payout(active_era: EraIndex) {
+        let total_dsm_staking_payout = T::MarketStakingPot::withdraw_staking_pot();
+        let duration = T::MarketStakingPotDuration::get();
+        let dsm_staking_payout_per_era = Perbill::from_rational_approximation(1, duration) * total_dsm_staking_payout;
+        // Reward starts from this era.
+        for i in 0..duration {
+            <ErasMarketStakingPayout<T>>::mutate(active_era + i, |payout| match *payout {
+                Some(amount) => *payout = Some(amount.saturating_add(dsm_staking_payout_per_era.clone())),
+                None => *payout = Some(dsm_staking_payout_per_era.clone())
+            });
+        }
+    }
+
     /// Apply previously-unapplied slashes on the beginning of a new era, after a delay.
     fn apply_unapplied_slashes(active_era: EraIndex) {
         let slash_defer_duration = T::SlashDeferDuration::get();
diff --git a/cstrml/staking/src/mock.rs b/cstrml/staking/src/mock.rs
index c62ae0c..363a0fc 100644
--- a/cstrml/staking/src/mock.rs
+++ b/cstrml/staking/src/mock.rs
@@ -47,6 +47,7 @@ thread_local! {
     static SLASH_DEFER_DURATION: RefCell<EraIndex> = RefCell::new(0);
     static OWN_WORKLOAD: RefCell<u128> = RefCell::new(0);
     static TOTAL_WORKLOAD: RefCell<u128> = RefCell::new(0);
+    static DSM_STAKING_PAYOUT: RefCell<Balance> = RefCell::new(0);
 }
 
 pub struct TestSessionHandler;
@@ -202,9 +203,12 @@ impl swork::Works<AccountId> for TestStaking {
     }
 }
 
-impl<AID> MarketInterface<AID> for TestStaking {
+impl<AID> MarketInterface<AID, BalanceOf<Test>> for TestStaking {
     fn upsert_replicas(_: &AID, _: &MerkleRoot, _: &SworkerAnchor, _: u32, _: &Option<BTreeSet<AID>>) -> bool { false }
     fn delete_replicas(_: &AID, _: &MerkleRoot, _: &SworkerAnchor, _: u32) -> bool { false }
+    fn withdraw_staking_pot() -> BalanceOf<Test> {
+        BalanceOf::<Test>::from(DSM_STAKING_PAYOUT.with(|v| *v.borrow()))
+    }
 }
 
 impl swork::Config for Test {
@@ -221,6 +225,7 @@ parameter_types! {
     pub const BondingDuration: EraIndex = 3;
     pub const MaxGuarantorRewardedPerValidator: u32 = 4;
     pub const SPowerRatio: u128 = 2_500;
+    pub const MarketStakingPotDuration: u32 = 5;
 }
 
 impl Config for Test {
@@ -240,6 +245,8 @@ impl Config for Test {
     type SlashCancelOrigin = frame_system::EnsureRoot<Self::AccountId>;
     type SessionInterface = Self;
     type SPowerRatio = SPowerRatio;
+    type MarketStakingPot = TestStaking;
+    type MarketStakingPotDuration = MarketStakingPotDuration;
     type WeightInfo = weight::WeightInfo;
 }
 
@@ -255,7 +262,8 @@ pub struct ExtBuilder {
     invulnerables: Vec<u128>,
     own_workload: u128,
     total_workload: u128,
-    staking_pot: Balance
+    staking_pot: Balance,
+    dsm_staking_payout: Balance
 }
 
 impl Default for ExtBuilder {
@@ -272,7 +280,8 @@ impl Default for ExtBuilder {
             invulnerables: vec![],
             own_workload: 3000,
             total_workload: 3000,
-            staking_pot: 1_000_000_000_000_000_000
+            staking_pot: 1_000_000_000_000_000_000,
+            dsm_staking_payout: 0
         }
     }
 }
@@ -326,11 +335,16 @@ impl ExtBuilder {
         self.staking_pot = amount;
         self
     }
+    pub fn dsm_staking_payout(mut self, amount: Balance) -> Self {
+        self.dsm_staking_payout = amount;
+        self
+    }
     pub fn set_associated_consts(&self) {
         EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit);
         SLASH_DEFER_DURATION.with(|v| *v.borrow_mut() = self.slash_defer_duration);
         OWN_WORKLOAD.with(|v| *v.borrow_mut() = self.own_workload);
         TOTAL_WORKLOAD.with(|v| *v.borrow_mut() = self.total_workload);
+        DSM_STAKING_PAYOUT.with(|v| *v.borrow_mut() = self.dsm_staking_payout);
     }
     pub fn build(self) -> sp_io::TestExternalities {
         self.set_associated_consts();
diff --git a/cstrml/swork/src/lib.rs b/cstrml/swork/src/lib.rs
index cce2b3f..afd9cb6 100644
--- a/cstrml/swork/src/lib.rs
+++ b/cstrml/swork/src/lib.rs
@@ -149,7 +149,7 @@ pub trait Config: system::Config {
     type Works: Works<Self::AccountId>;
 
     /// Interface for interacting with a market module.
-    type MarketInterface: MarketInterface<Self::AccountId>;
+    type MarketInterface: MarketInterface<Self::AccountId, BalanceOf<Self>>;
 
     /// Weight information for extrinsics in this pallet.
     type WeightInfo: WeightInfo;
diff --git a/node/src/main.rs b/node/src/main.rs
index 4f77c0b..f4c0f99 100644
--- a/node/src/main.rs
+++ b/node/src/main.rs
@@ -1,6 +1,5 @@
 // Copyright (C) 2019-2021 Crust Network Technologies Ltd.
 // This file is part of Crust.
-
 #![warn(missing_docs)]
 
 mod chain_spec;
diff --git a/primitives/src/traits.rs b/primitives/src/traits.rs
index 60af2bc..0a382d6 100644
--- a/primitives/src/traits.rs
+++ b/primitives/src/traits.rs
@@ -23,11 +23,13 @@ pub trait SworkerInterface<AccountId> {
 }
 
 /// Means for interacting with a specialized version of the `market` trait.
-pub trait MarketInterface<AccountId> {
+pub trait MarketInterface<AccountId, Balance> {
 	// used for `added_files`
 	// return is_added
 	fn upsert_replicas(who: &AccountId, cid: &MerkleRoot, anchor: &SworkerAnchor, valid_at: BlockNumber, members: &Option<BTreeSet<AccountId>>) -> bool;
 	// used for `delete_files`
 	// return is_deleted
 	fn delete_replicas(who: &AccountId, cid: &MerkleRoot, anchor: &SworkerAnchor, curr_bn: BlockNumber) -> bool;
+	// used for distribute market staking payout
+	fn withdraw_staking_pot() -> Balance;
 }
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index fb24bac..4e38280 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -397,6 +397,8 @@ parameter_types! {
     pub const SPowerRatio: u128 = 100;
     // 64 guarantors for one validator.
     pub const MaxGuarantorRewardedPerValidator: u32 = 64;
+    // 60 eras means 15 days if era = 6 hours
+    pub const MarketStakingPotDuration: u32 = 60;
 }
 
 impl staking::Config for Runtime {
@@ -419,6 +421,8 @@ impl staking::Config for Runtime {
     type SlashCancelOrigin = frame_system::EnsureRoot<Self::AccountId>;
     type SessionInterface = Self;
     type SPowerRatio = SPowerRatio;
+    type MarketStakingPot = Market;
+    type MarketStakingPotDuration = MarketStakingPotDuration;
     type WeightInfo = staking::weight::WeightInfo;
 }
 


Test patch:
diff --git a/cstrml/market/src/tests.rs b/cstrml/market/src/tests.rs
index 37aa207..3cbf295 100644
--- a/cstrml/market/src/tests.rs
+++ b/cstrml/market/src/tests.rs
@@ -1513,3 +1513,53 @@ fn update_price_should_work() {
     });
 }
 
+/// Withdraw staking pot should work
+#[test]
+fn withdraw_staking_pot_should_work() {
+    new_test_ext().execute_with(|| {
+        // generate 50 blocks first
+        run_to_block(50);
+
+        let source = ALICE;
+        let merchant = MERCHANT;
+
+        let cid =
+            hex::decode("4e2883ddcbc77cf19979770d756fd332d0c8f815f9de646636169e460e6af6ff").unwrap();
+        let file_size = 100; // should less than
+        let reserved_pot = Market::reserved_pot();
+        let staking_pot = Market::staking_pot();
+        let storage_pot = Market::storage_pot();
+        assert_eq!(Balances::free_balance(&staking_pot), 0);
+        let _ = Balances::make_free_balance_be(&source, 20000);
+        let _ = Balances::make_free_balance_be(&merchant, 200);
+
+        assert_ok!(Market::register(Origin::signed(merchant.clone()), 60));
+
+        assert_ok!(Market::place_storage_order(
+            Origin::signed(source), cid.clone(),
+            file_size, 0, false
+        ));
+
+        assert_eq!(Market::files(&cid).unwrap_or_default(), (
+            FileInfo {
+                file_size,
+                expired_on: 50,
+                claimed_at: 50,
+                amount: 360, // ( 1000 + 1000 * 1 + 0 ) * 0.2
+                expected_replica_count: 4,
+                reported_replica_count: 0,
+                replicas: vec![]
+            },
+            UsedInfo {
+                used_size: file_size,
+                groups: BTreeSet::from_iter(vec![].into_iter())
+            })
+        );
+        assert_eq!(Balances::free_balance(&reserved_pot), 200);
+        assert_eq!(Balances::free_balance(&staking_pot), 1440);
+        assert_eq!(Balances::free_balance(&storage_pot), 360);
+
+        assert_eq!(Market::withdraw_staking_pot(), 1439);
+        assert_eq!(Balances::free_balance(&staking_pot), 1);
+    });
+}
diff --git a/cstrml/staking/src/tests.rs b/cstrml/staking/src/tests.rs
index 5295979..4b80a82 100644
--- a/cstrml/staking/src/tests.rs
+++ b/cstrml/staking/src/tests.rs
@@ -402,6 +402,84 @@ fn multi_era_reward_should_work() {
         });
 }
 
+#[test]
+fn era_reward_with_dsm_staking_pot_should_work() {
+    // Should check that:
+    // The value of current_session_reward is set at the end of each era, based on
+    // total_stakes and session_reward.
+    let dsm_staking_payout_per_era: Balance = 100_000_000_000_000;
+    ExtBuilder::default()
+        .guarantee(false)
+        .own_workload(u128::max_value())
+        .staking_pot(400_000_000_000_000)
+        .dsm_staking_payout(dsm_staking_payout_per_era * 5)
+        .build()
+        .execute_with(|| {
+            let init_balance_10 = Balances::total_balance(&10);
+            let init_balance_21 = Balances::total_balance(&21);
+
+            // Set payee to controller
+            assert_ok!(Staking::set_payee(
+                Origin::signed(10),
+                RewardDestination::Controller
+            ));
+
+            // Compute now as other parameter won't change
+            let total_authoring_payout = Staking::authoring_rewards_in_era();
+            let total_staking_payout_0 = Staking::staking_rewards_in_era(Staking::current_era().unwrap_or(0));
+            assert!(total_staking_payout_0 > 10); // Test is meaningful if reward something
+            assert_eq!(Staking::eras_total_stakes(0), 2001);
+            <Module<Test>>::reward_by_ids(vec![(21, 1)]);
+
+            start_session(0, true);
+            start_session(1, true);
+            start_session(2, true);
+            start_session(3, true);
+            payout_all_stakers(0);
+
+            assert_eq!(Staking::current_era().unwrap_or(0), 1);
+            assert_eq!(Staking::eras_total_stakes(1), 2001);
+            // rewards may round to 0.000001
+            assert_eq!(
+                Balances::total_balance(&10) / 10000000,
+                (init_balance_10 + total_staking_payout_0 * 1000 / 2001 + dsm_staking_payout_per_era * 1000 / 2001) / 10000000
+            );
+            let stakes_21 = Balances::total_balance(&21);
+            let stakes_31 = Balances::total_balance(&31);
+            // candidates should have rewards
+            assert_eq!(
+                stakes_21 / 10000000,
+                (init_balance_21 + total_authoring_payout + total_staking_payout_0 * 1000 / 2001 + dsm_staking_payout_per_era * 1000 / 2001) / 10000000
+            );
+
+            start_session(4, true);
+
+            <Module<Test>>::reward_by_ids(vec![(21, 101)]); // meaningless points
+            // new era is triggered here.
+            start_session(5, true);
+            start_session(6, true);
+            payout_all_stakers(1);
+            assert_eq!(Staking::eras_total_stakes(2), 198396500634946);
+            // pay time
+            // staking pot is not enough
+            // only dsm staking payout
+            assert_eq!(
+                Balances::total_balance(&10) / 100000000,
+                (init_balance_10 + total_staking_payout_0 * 1000 / 2001
+                    + (dsm_staking_payout_per_era * 1000 / 2001) + (dsm_staking_payout_per_era * 2 * 1000 / 2001)) / 100000000
+            );
+            assert_eq!(
+                Balances::total_balance(&21) / 10000000,
+                (stakes_21 + (dsm_staking_payout_per_era * 2 * 1000 / 2001)) / 10000000
+            );
+            assert_eq!(
+                Balances::total_balance(&31) / 10000000,
+                (stakes_31 + (dsm_staking_payout_per_era * 2 / 2001)) / 10000000
+            );
+        });
+}
+
+
 #[test]
 fn era_reward_should_fail_due_to_insufficient_staking_pot() {
     // Should check that: