Skip to content

Commit f660087

Browse files
committed
Feat/reward pool history (#1710)
The goal of this PR is to implement storage of reward pool history. Closes #1710
1 parent 90943ed commit f660087

File tree

10 files changed

+177
-79
lines changed

10 files changed

+177
-79
lines changed

e2e/capacity/staking.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,13 @@ describe("Capacity Staking Tests", function () {
208208
});
209209

210210
describe("when attempting to stake below the minimum staking requirements", async function () {
211-
it("should fail to stake for InsufficientStakingAmount", async function () {
211+
it("should fail to stake for StakingAmountBelowMinimum", async function () {
212212
let stakingKeys = createKeys("stakingKeys");
213213
let providerId = await createMsaAndProvider(fundingSource, stakingKeys, "stakingKeys", 150n * CENTS);
214214
let stakeAmount = 1500n;
215215

216216
const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, stakeAmount);
217-
await assert.rejects(failStakeObj.fundAndSend(fundingSource), { name: "InsufficientStakingAmount" });
217+
await assert.rejects(failStakeObj.fundAndSend(fundingSource), { name: "StakingAmountBelowMinimum" });
218218
});
219219
});
220220

@@ -224,7 +224,7 @@ describe("Capacity Staking Tests", function () {
224224
let providerId = await createMsaAndProvider(fundingSource, stakingKeys, "stakingKeys", );
225225

226226
const failStakeObj = ExtrinsicHelper.stake(stakingKeys, providerId, 0);
227-
await assert.rejects(failStakeObj.fundAndSend(fundingSource), { name: "ZeroAmountNotAllowed" });
227+
await assert.rejects(failStakeObj.fundAndSend(fundingSource), { name: "StakingAmountBelowMinimum" });
228228
});
229229
});
230230

pallets/capacity/src/lib.rs

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
rustdoc::invalid_codeblock_attributes,
4949
missing_docs
5050
)]
51+
use sp_std::ops::{Add, Mul};
5152

5253
use frame_support::{
5354
dispatch::DispatchResult,
@@ -60,7 +61,6 @@ use sp_runtime::{
6061
traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero},
6162
ArithmeticError, DispatchError, Perbill,
6263
};
63-
use sp_std::ops::Mul;
6464

6565
pub use common_primitives::{
6666
capacity::{Nontransferable, Replenishable, TargetValidator},
@@ -90,12 +90,12 @@ type BalanceOf<T> =
9090
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
9191

9292
const STAKING_ID: LockIdentifier = *b"netstkng";
93-
9493
#[frame_support::pallet]
9594
pub mod pallet {
9695
use super::*;
9796
use codec::EncodeLike;
9897

98+
use common_primitives::capacity::StakingType;
9999
use frame_support::{pallet_prelude::*, Twox64Concat};
100100
use frame_system::pallet_prelude::*;
101101
use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeDisplay};
@@ -177,7 +177,7 @@ pub mod pallet {
177177

178178
/// The maximum number of eras over which one can claim rewards
179179
#[pallet::constant]
180-
type StakingRewardsPastErasMax: Get<u32>;
180+
type StakingRewardsPastErasMax: Get<Self::RewardEra>;
181181

182182
/// The StakingRewardsProvider used by this pallet in a given runtime
183183
type RewardsProvider: StakingRewardsProvider<Self>;
@@ -186,7 +186,7 @@ pub mod pallet {
186186
/// thaw chunk to expire. If the staker has called change_staking_target MaxUnlockingChunks
187187
/// times, then at least one of the chunks must have expired before the next call
188188
/// will succeed.
189-
type ChangeStakingTargetThawEras: Get<u32>;
189+
type ChangeStakingTargetThawEras: Get<Self::RewardEra>;
190190
}
191191

192192
/// Storage for keeping a ledger of staked token amounts for accounts.
@@ -242,8 +242,9 @@ pub mod pallet {
242242
pub type EpochLength<T: Config> =
243243
StorageValue<_, T::BlockNumber, ValueQuery, EpochLengthDefault<T>>;
244244

245-
/// Information about the current staking reward era.
245+
/// Information about the current staking reward era. Checked every block.
246246
#[pallet::storage]
247+
#[pallet::whitelist_storage]
247248
#[pallet::getter(fn get_current_era)]
248249
pub type CurrentEraInfo<T: Config> =
249250
StorageValue<_, RewardEraInfo<T::RewardEra, T::BlockNumber>, ValueQuery>;
@@ -252,10 +253,8 @@ pub mod pallet {
252253
#[pallet::storage]
253254
#[pallet::getter(fn get_reward_pool_for_era)]
254255
pub type StakingRewardPool<T: Config> =
255-
StorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo<BalanceOf<T>>>;
256+
CountedStorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo<BalanceOf<T>>>;
256257

257-
// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
258-
// method.
259258
#[pallet::pallet]
260259
pub struct Pallet<T>(_);
261260

@@ -356,6 +355,8 @@ pub mod pallet {
356355
IneligibleForPayoutInEraRange,
357356
/// Attempted to retarget but from and to Provider MSA Ids were the same
358357
CannotRetargetToSameProvider,
358+
/// Rewards were already paid out this era
359+
AlreadyClaimedRewardsThisEra,
359360
}
360361

361362
#[pallet::hooks]
@@ -448,10 +449,9 @@ pub mod pallet {
448449
requested_amount: BalanceOf<T>,
449450
) -> DispatchResult {
450451
let unstaker = ensure_signed(origin)?;
451-
Self::ensure_can_unstake(&unstaker)?;
452-
453452
ensure!(requested_amount > Zero::zero(), Error::<T>::UnstakedAmountIsZero);
454453

454+
Self::ensure_can_unstake(&unstaker)?;
455455
let actual_amount = Self::decrease_active_staking_balance(&unstaker, requested_amount)?;
456456
let capacity_reduction = Self::reduce_capacity(&unstaker, target, actual_amount)?;
457457

@@ -487,13 +487,16 @@ pub mod pallet {
487487
/// This adds a chunk to `StakingAccountDetails.stake_change_unlocking chunks`, up to `T::MaxUnlockingChunks`.
488488
/// The staked amount and Capacity generated by `amount` originally targeted to the `from` MSA Id is reassigned to the `to` MSA Id.
489489
/// Does not affect unstaking process or additional stake amounts.
490+
/// Changing a staking target to a Provider when Origin has nothing staked them will retain the staking type.
491+
/// Changing a staking target to a Provider when Origin has any amount staked to them will error if the staking types are not the same.
490492
/// ### Errors
491493
/// - [`Error::NotAStakingAccount`] if origin does not have a staking account
492494
/// - [`Error::MaxUnlockingChunksExceeded`] if `stake_change_unlocking_chunks` == `T::MaxUnlockingChunks`
493495
/// - [`Error::StakerTargetRelationshipNotFound`] if `from` is not a target for Origin's staking account.
494496
/// - [`Error::StakingAmountBelowMinimum`] if `amount` to retarget is below the minimum staking amount.
495497
/// - [`Error::InsufficientStakingBalance`] if `amount` to retarget exceeds what the staker has targeted to `from` MSA Id.
496498
/// - [`Error::InvalidTarget`] if `to` does not belong to a registered Provider.
499+
/// - [`Error::CannotChangeStakingType`] if origin already has funds staked for `to` and the staking type for `from` is different.
497500
#[pallet::call_index(4)]
498501
#[pallet::weight(T::WeightInfo::unstake())]
499502
pub fn change_staking_target(
@@ -764,18 +767,41 @@ impl<T: Config> Pallet<T> {
764767
}
765768

766769
fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight {
767-
let current_era_info: RewardEraInfo<T::RewardEra, T::BlockNumber> = Self::get_current_era();
770+
let current_era_info: RewardEraInfo<T::RewardEra, T::BlockNumber> = Self::get_current_era(); // 1r
771+
768772
if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() {
769-
CurrentEraInfo::<T>::set(RewardEraInfo {
773+
let new_era_info = RewardEraInfo {
770774
era_index: current_era_info.era_index.saturating_add(One::one()),
771775
started_at: current_block,
772-
});
773-
// TODO: modify reads/writes as needed when RewardPoolInfo stuff is added
776+
};
777+
778+
let current_reward_pool_info =
779+
Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r
780+
781+
let past_eras_max = T::StakingRewardsPastErasMax::get();
782+
let entries: u32 = StakingRewardPool::<T>::count(); // 1r
783+
784+
if past_eras_max.eq(&entries.into()) {
785+
let earliest_era =
786+
current_era_info.era_index.saturating_sub(past_eras_max).add(One::one());
787+
StakingRewardPool::<T>::remove(earliest_era); // 1w
788+
}
789+
CurrentEraInfo::<T>::set(new_era_info); // 1w
790+
791+
let total_reward_pool =
792+
T::RewardsProvider::reward_pool_size(current_reward_pool_info.total_staked_token);
793+
let new_reward_pool = RewardPoolInfo {
794+
total_staked_token: current_reward_pool_info.total_staked_token,
795+
total_reward_pool,
796+
unclaimed_balance: total_reward_pool,
797+
};
798+
StakingRewardPool::<T>::insert(new_era_info.era_index, new_reward_pool); // 1w
799+
774800
T::WeightInfo::on_initialize()
775-
.saturating_add(T::DbWeight::get().reads(1))
776-
.saturating_add(T::DbWeight::get().writes(1))
801+
.saturating_add(T::DbWeight::get().reads(3))
802+
.saturating_add(T::DbWeight::get().writes(3))
777803
} else {
778-
T::DbWeight::get().reads(2).saturating_add(RocksDbWeight::get().writes(1))
804+
T::DbWeight::get().reads(1)
779805
}
780806
}
781807

@@ -796,7 +822,7 @@ impl<T: Config> Pallet<T> {
796822
Self::get_staking_account_for(staker).ok_or(Error::<T>::NotAStakingAccount)?;
797823

798824
let current_era: T::RewardEra = Self::get_current_era().era_index;
799-
let thaw_at = current_era.saturating_add(T::ChangeStakingTargetThawEras::get().into());
825+
let thaw_at = current_era.saturating_add(T::ChangeStakingTargetThawEras::get());
800826
staking_account_details.update_stake_change_unlocking(amount, &thaw_at, &current_era)?;
801827
Self::set_staking_account(staker, &staking_account_details);
802828
Ok(())
@@ -814,15 +840,22 @@ impl<T: Config> Pallet<T> {
814840
let capacity_withdrawn = Self::reduce_capacity(staker, *from_msa, *amount)?;
815841

816842
let mut to_msa_target = Self::get_target_for(staker, to_msa).unwrap_or_default();
843+
844+
if to_msa_target.amount.is_zero() {
845+
// it's a new StakingTargetDetails record.
846+
to_msa_target.staking_type = staking_type.clone();
847+
} else {
848+
// make sure they are not retargeting to a StakingTargetDetails with a different staking
849+
// type, otherwise it could interfere with staking rewards.
850+
ensure!(
851+
to_msa_target.staking_type.eq(staking_type),
852+
Error::<T>::CannotChangeStakingType
853+
);
854+
}
817855
to_msa_target
818856
.deposit(*amount, capacity_withdrawn)
819857
.ok_or(ArithmeticError::Overflow)?;
820858

821-
// TODO: document
822-
// if someone wants to switch staking type they must unstake completely and restake regardless of
823-
// whether it is with an existing or new provider.
824-
to_msa_target.staking_type = staking_type.clone();
825-
826859
let mut capacity_details = Self::get_capacity_for(to_msa).unwrap_or_default();
827860
capacity_details
828861
.deposit(amount, &capacity_withdrawn)
@@ -925,19 +958,13 @@ impl<T: Config> StakingRewardsProvider<T> for Pallet<T> {
925958

926959
// Calculate the size of the reward pool for the current era, based on current staked token
927960
// and the other determined factors of the current economic model
928-
fn reward_pool_size() -> Result<BalanceOf<T>, DispatchError> {
929-
let current_era_info = CurrentEraInfo::<T>::get();
930-
let current_staked =
931-
StakingRewardPool::<T>::get(current_era_info.era_index).unwrap_or_default();
932-
if current_staked.total_staked_token.is_zero() {
933-
return Ok(BalanceOf::<T>::zero())
961+
fn reward_pool_size(total_staked: BalanceOf<T>) -> BalanceOf<T> {
962+
if total_staked.is_zero() {
963+
return BalanceOf::<T>::zero()
934964
}
935965

936966
// For now reward pool size is set to 10% of total staked token
937-
Ok(current_staked
938-
.total_staked_token
939-
.checked_div(&BalanceOf::<T>::from(10u8))
940-
.unwrap_or_default())
967+
total_staked.checked_div(&BalanceOf::<T>::from(10u8)).unwrap_or_default()
941968
}
942969

943970
// Performs range checks plus a reward calculation based on economic model for the era range
@@ -946,9 +973,8 @@ impl<T: Config> StakingRewardsProvider<T> for Pallet<T> {
946973
from_era: T::RewardEra,
947974
to_era: T::RewardEra,
948975
) -> Result<BalanceOf<T>, DispatchError> {
949-
let max_eras = T::RewardEra::from(T::StakingRewardsPastErasMax::get());
950976
let era_range = from_era.saturating_sub(to_era);
951-
ensure!(era_range.le(&max_eras), Error::<T>::EraOutOfRange);
977+
ensure!(era_range.le(&T::StakingRewardsPastErasMax::get()), Error::<T>::EraOutOfRange);
952978
ensure!(from_era.le(&to_era), Error::<T>::EraOutOfRange);
953979
let current_era_info = Self::get_current_era();
954980
ensure!(to_era.lt(&current_era_info.era_index), Error::<T>::EraOutOfRange);

pallets/capacity/src/tests/change_staking_target_tests.rs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use super::{mock::*, testing_utils::*};
1+
use super::{
2+
mock::*,
3+
testing_utils::{setup_provider, staking_events},
4+
};
25
use crate::{
36
BalanceOf, CapacityDetails, Config, CurrentEraInfo, Error, Event, RewardEraInfo,
47
StakingAccountDetails, StakingAccountLedger, StakingTargetDetails,
@@ -13,22 +16,6 @@ use common_primitives::{
1316
use frame_support::{assert_noop, assert_ok, traits::Get};
1417

1518
// staker is unused unless amount > 0
16-
fn setup_provider(staker: &u64, target: &MessageSourceId, amount: &u64, staking_type: StakingType) {
17-
let provider_name = String::from("Cst-") + target.to_string().as_str();
18-
register_provider(*target, provider_name);
19-
if amount.gt(&0u64) {
20-
assert_ok!(Capacity::stake(
21-
RuntimeOrigin::signed(staker.clone()),
22-
*target,
23-
*amount,
24-
staking_type.clone()
25-
));
26-
let target = Capacity::get_target_for(staker, target).unwrap();
27-
assert_eq!(target.amount, *amount);
28-
assert_eq!(target.staking_type, staking_type);
29-
}
30-
}
31-
3219
type TestCapacityDetails = CapacityDetails<BalanceOf<Test>, u32>;
3320
type TestTargetDetails = StakingTargetDetails<Test>;
3421

@@ -60,6 +47,7 @@ fn assert_target_details(
6047
let from_target_details = Capacity::get_target_for(staker, msa_id).unwrap();
6148
assert_eq!(from_target_details, expected_from_target_details);
6249
}
50+
6351
#[test]
6452
fn do_retarget_happy_path() {
6553
new_test_ext().execute_with(|| {
@@ -160,6 +148,7 @@ fn assert_total_capacity(msas: Vec<MessageSourceId>, total: u64) {
160148
.fold(0, |a, b| a + b);
161149
assert_eq!(total, sum);
162150
}
151+
163152
#[test]
164153
fn check_retarget_multiple_stakers() {
165154
new_test_ext().execute_with(|| {
Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,74 @@
1-
use super::mock::*;
2-
use crate::{
3-
tests::testing_utils::{run_to_block, system_run_to_block},
4-
CurrentEraInfo, RewardEraInfo,
1+
use super::{
2+
mock::*,
3+
testing_utils::{run_to_block, system_run_to_block},
54
};
5+
use crate::{Config, CurrentEraInfo, RewardEraInfo, RewardPoolInfo, StakingRewardPool};
6+
use sp_core::Get;
67

78
#[test]
8-
fn start_new_era_if_needed() {
9+
fn start_new_era_if_needed_updates_era_info_and_limits_reward_pool_size() {
910
new_test_ext().execute_with(|| {
1011
CurrentEraInfo::<Test>::set(RewardEraInfo { era_index: 1, started_at: 0 });
12+
StakingRewardPool::<Test>::insert(
13+
1,
14+
RewardPoolInfo {
15+
total_staked_token: 10_000,
16+
total_reward_pool: 1_000,
17+
unclaimed_balance: 1_000,
18+
},
19+
);
1120
system_run_to_block(9);
12-
run_to_block(10);
13-
let mut current_era_info = CurrentEraInfo::<Test>::get();
14-
assert_eq!(current_era_info.era_index, 2u32);
15-
assert_eq!(current_era_info.started_at, 10u32);
21+
for i in 1..4 {
22+
let block_decade = i * 10;
23+
run_to_block(block_decade);
24+
25+
let current_era_info = CurrentEraInfo::<Test>::get();
1626

17-
system_run_to_block(19);
18-
run_to_block(20);
19-
current_era_info = CurrentEraInfo::<Test>::get();
20-
assert_eq!(current_era_info.era_index, 3u32);
21-
assert_eq!(current_era_info.started_at, 20u32);
27+
let expected_era = i + 1;
28+
assert_eq!(current_era_info.era_index, expected_era);
29+
assert_eq!(current_era_info.started_at, block_decade);
30+
let past_eras_max: u32 = <Test as Config>::StakingRewardsPastErasMax::get();
31+
assert!(StakingRewardPool::<Test>::count().le(&past_eras_max));
32+
system_run_to_block(block_decade + 9);
33+
}
2234
})
2335
}
36+
37+
#[test]
38+
fn start_new_era_if_needed_updates_reward_pool() {
39+
new_test_ext().execute_with(|| {
40+
CurrentEraInfo::<Test>::set(RewardEraInfo { era_index: 1, started_at: 0 });
41+
StakingRewardPool::<Test>::insert(
42+
1,
43+
RewardPoolInfo {
44+
total_staked_token: 10_000,
45+
total_reward_pool: 1_000,
46+
unclaimed_balance: 1_000,
47+
},
48+
);
49+
system_run_to_block(8);
50+
51+
// TODO: Provider boost, after staking updates reward pool info #1699
52+
// let staker = 10_000;
53+
// let provider_msa: MessageSourceId = 1;
54+
// let stake_amount = 600u64;
55+
// setup_provider(&staker, &provider_msa, &stake_amount, ProviderBoost);
56+
57+
system_run_to_block(9);
58+
run_to_block(10);
59+
assert_eq!(StakingRewardPool::<Test>::count(), 2);
60+
let current_reward_pool_info = StakingRewardPool::<Test>::get(2).unwrap();
61+
assert_eq!(
62+
current_reward_pool_info,
63+
RewardPoolInfo {
64+
total_staked_token: 10_000,
65+
total_reward_pool: 1_000,
66+
unclaimed_balance: 1_000,
67+
}
68+
);
69+
70+
// TODO: after staking updates reward pool info #1699
71+
// system_run_to_block(19);
72+
// run_to_block(20);
73+
});
74+
}

0 commit comments

Comments
 (0)