Skip to content

Commit

Permalink
Feat/staking rewards rewards provider #1572 (#1598)
Browse files Browse the repository at this point in the history
# Goal
The goal of this PR is to implement a really basic version of the
StakingRewardsProvider in the Capacity pallet and in the test mock,
neither of which is actively used.

Closes #1572

# Discussion
Does not include anything to do with setting and storing RewardPoolInfo
when each new Era starts.

# Checklist
- [x] Design doc(s) updated
- [x] Tests added
  • Loading branch information
shannonwells committed Oct 11, 2023
1 parent 6d3b694 commit bbd773a
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 97 deletions.
2 changes: 1 addition & 1 deletion common/primitives/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub type Signature = MultiSignature;
/// Index of a transaction in the chain.
pub type Index = u32;

/// the time period in blocks that Staking Rewards are based upon
/// the type used for the index of the Staking Reward time period
pub type RewardEra = u32;

/// A hash of some data used by the chain.
Expand Down
15 changes: 5 additions & 10 deletions designdocs/capacity_staking_rewards_implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ This document outlines how to implement the Staking for Rewards feature describe
1. **Capacity**: the non-transferrable utility token which can be used only to pay for certain Frequency transactions.
1. **Account**: a Frequency System Account controlled by a private key and addressed by a public key, having at least a minimum balance (currently 0.01 FRQCY).
1. **Stake** (verb): to lock some amount of a token against transfer for a period of time in exchange for some reward.
1. **RewardEra**: the time period (TBD in blocks or Capacity Epochs) that Staking Rewards are based upon. RewardEra is to distinguish it easily from Substrate's staking pallet Era.
1. **RewardEra**: the time period (TBD in blocks or Capacity Epochs) that Staking Rewards are based upon. RewardEra is to distinguish it easily from Substrate's staking pallet Era, or the index of said time period.
1. **Staking Reward**: a per-RewardEra share of a staking reward pool of FRQCY tokens for a given staking account.
1. **Reward Pool**: a fixed amount of FRQCY that can be minted for rewards each RewardEra and distributed to stakers.
1. **StakingRewardsProvider**: a trait that encapsulates the economic model for staking rewards, providing functionality for calculating the reward pool and staking rewards.

## Staking Token Rewards

Expand Down Expand Up @@ -101,10 +102,8 @@ pub struct StakingRewardClaim<T: Config> {

pub trait StakingRewardsProvider<T: Config> {

/// Return the size of the reward pool for the given era, in token
/// Errors:
/// - EraOutOfRange when `era` is prior to the history retention limit, or greater than the current RewardEra.
fn reward_pool_size(era: EraOf<T>) -> BalanceOf<T>;
/// Calculate the size of the reward pool for the given era, in token
fn reward_pool_size() -> BalanceOf<T>;

/// Return the total unclaimed reward in token for `account_id` for `fromEra` --> `toEra`, inclusive
/// Errors:
Expand Down Expand Up @@ -159,14 +158,10 @@ pub trait Config: frame_system::Config {
This is the necessary information about the reward pool for a given Reward Era and how it's stored.
```rust
pub struct RewardPoolInfo<T: Config> {
/// the total staked for rewards in the associated RewardEra
total_staked_token: BalanceOf<T>,
/// the remaining rewards balance to be claimed
unclaimed_balance: BalanceOf<T>
}
/// Reward Pool history
#[pallet::storage]
#[pallet::getter(fn get_reward_pool_for_era)]
/// Reward Pool history
pub type StakingRewardPool<T: Config> = <StorageMap<_, Twox64Concat, RewardEra, RewardPoolInfo<T>;
```

Expand Down
78 changes: 67 additions & 11 deletions pallets/capacity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use frame_support::{
};

use sp_runtime::{
traits::{CheckedAdd, Saturating, Zero},
traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero},
ArithmeticError, DispatchError, Perbill,
};
use sp_std::ops::Mul;
Expand Down Expand Up @@ -92,6 +92,7 @@ const STAKING_ID: LockIdentifier = *b"netstkng";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use codec::EncodeLike;

use frame_support::{pallet_prelude::*, Twox64Concat};
use frame_system::pallet_prelude::*;
Expand Down Expand Up @@ -164,6 +165,8 @@ pub mod pallet {
+ Copy
+ sp_std::hash::Hash
+ MaxEncodedLen
+ EncodeLike
+ Into<BalanceOf<Self>>
+ TypeInfo;

/// The number of blocks in a Staking Era
Expand All @@ -174,8 +177,8 @@ pub mod pallet {
#[pallet::constant]
type StakingRewardsPastErasMax: Get<u32>;

// The StakingRewardsProvider used by this pallet in a given runtime
// type RewardsProvider: StakingRewardsProvider<Self>;
/// The StakingRewardsProvider used by this pallet in a given runtime
type RewardsProvider: StakingRewardsProvider<Self>;
}

/// Storage for keeping a ledger of staked token amounts for accounts.
Expand Down Expand Up @@ -231,12 +234,18 @@ pub mod pallet {
pub type EpochLength<T: Config> =
StorageValue<_, T::BlockNumber, ValueQuery, EpochLengthDefault<T>>;

/// Information for the current era
/// Information about the current staking reward era.
#[pallet::storage]
#[pallet::getter(fn get_current_era)]
pub type CurrentEraInfo<T: Config> =
StorageValue<_, RewardEraInfo<T::RewardEra, T::BlockNumber>, ValueQuery>;

/// Reward Pool history
#[pallet::storage]
#[pallet::getter(fn get_reward_pool_for_era)]
pub type StakingRewardPool<T: Config> =
StorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo<BalanceOf<T>>>;

// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
// method.
#[pallet::pallet]
Expand Down Expand Up @@ -342,7 +351,6 @@ pub mod pallet {
/// ### Errors
///
/// - Returns Error::ZeroAmountNotAllowed if the staker is attempting to stake a zero amount.

/// - Returns Error::InvalidTarget if attempting to stake to an invalid target.
/// - Returns Error::InsufficientStakingAmount if attempting to stake an amount below the minimum amount.
/// - Returns Error::CannotChangeStakingType if the staking account exists and staking_type is different
Expand Down Expand Up @@ -575,8 +583,8 @@ impl<T: Config> Pallet<T> {
ensure!(amount <= staking_account.active, Error::<T>::AmountToUnstakeExceedsAmountStaked);

let current_epoch: T::EpochNumber = Self::get_current_epoch();
let thaw_at =
current_epoch.saturating_add(T::EpochNumber::from(T::UnstakingThawPeriod::get()));
let thaw_period = T::UnstakingThawPeriod::get();
let thaw_at = current_epoch.saturating_add(thaw_period.into());

let unstake_result = staking_account.withdraw(amount, thaw_at)?;

Expand Down Expand Up @@ -631,7 +639,7 @@ impl<T: Config> Pallet<T> {
Self::get_epoch_length()
{
let current_epoch = Self::get_current_epoch();
CurrentEpoch::<T>::set(current_epoch.saturating_add(1u32.into()));
CurrentEpoch::<T>::set(current_epoch.saturating_add(One::one()));
CurrentEpochInfo::<T>::set(EpochInfo { epoch_start: current_block });
T::WeightInfo::on_initialize()
.saturating_add(T::DbWeight::get().reads(1))
Expand All @@ -644,10 +652,10 @@ impl<T: Config> Pallet<T> {

fn start_new_reward_era_if_needed(current_block: T::BlockNumber) -> Weight {
let current_era_info: RewardEraInfo<T::RewardEra, T::BlockNumber> = Self::get_current_era();
if current_block.saturating_sub(current_era_info.era_start) >= T::EraLength::get().into() {
if current_block.saturating_sub(current_era_info.started_at) >= T::EraLength::get().into() {
CurrentEraInfo::<T>::set(RewardEraInfo {
current_era: current_era_info.current_era.saturating_add(1u8.into()),
era_start: current_block,
era_index: current_era_info.era_index.saturating_add(One::one()),
started_at: current_block,
});
// TODO: modify reads/writes as needed when RewardPoolInfo stuff is added
T::WeightInfo::on_initialize()
Expand Down Expand Up @@ -735,3 +743,51 @@ impl<T: Config> Replenishable for Pallet<T> {
false
}
}

impl<T: Config> StakingRewardsProvider<T> for Pallet<T> {
type AccountId = T::AccountId;
type RewardEra = T::RewardEra;
type Hash = T::Hash;

// Calculate the size of the reward pool for the current era, based on current staked token
// and the other determined factors of the current economic model
// Currently set to 10% of total staked token.
fn reward_pool_size() -> Result<BalanceOf<T>, DispatchError> {
let current_era_info = CurrentEraInfo::<T>::get();
let current_staked =
StakingRewardPool::<T>::get(current_era_info.era_index).unwrap_or_default();
if current_staked.total_staked_token.is_zero() {
return Ok(BalanceOf::<T>::zero())
}
Ok(current_staked
.total_staked_token
.checked_div(&BalanceOf::<T>::from(10u8))
.unwrap_or_default())
}

// Performs range checks plus a reward calculation based on economic model for the era range
// Currently just rewards 1 unit per era for a valid range since there is no history storage
fn staking_reward_total(
_account_id: T::AccountId,
from_era: T::RewardEra,
to_era: T::RewardEra,
) -> Result<BalanceOf<T>, DispatchError> {
let max_eras = T::RewardEra::from(T::StakingRewardsPastErasMax::get());
let era_range = from_era.saturating_sub(to_era);
ensure!(era_range.le(&max_eras), Error::<T>::EraOutOfRange);
ensure!(from_era.le(&to_era), Error::<T>::EraOutOfRange);
let current_era_info = Self::get_current_era();
ensure!(to_era.lt(&current_era_info.era_index), Error::<T>::EraOutOfRange);
let per_era = BalanceOf::<T>::one();
let num_eras = to_era.saturating_sub(from_era);
Ok(per_era.saturating_mul(num_eras.into()))
}

fn validate_staking_reward_claim(
_account_id: T::AccountId,
_proof: T::Hash,
_payload: StakingRewardClaim<T>,
) -> bool {
true
}
}
14 changes: 6 additions & 8 deletions pallets/capacity/src/tests/eras_tests.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
use super::mock::*;
use crate::{
tests::testing_utils::{run_to_block, system_run_to_block},
Config, CurrentEraInfo, Error, Event, RewardEraInfo,
CurrentEraInfo, RewardEraInfo,
};

use frame_support::traits::Get;

#[test]
fn start_new_era_if_needed() {
new_test_ext().execute_with(|| {
CurrentEraInfo::<Test>::set(RewardEraInfo { current_era: 1, era_start: 0 });
CurrentEraInfo::<Test>::set(RewardEraInfo { era_index: 1, started_at: 0 });
system_run_to_block(9);
run_to_block(10);
let mut current_era_info = CurrentEraInfo::<Test>::get();
assert_eq!(current_era_info.current_era, 2u32);
assert_eq!(current_era_info.era_start, 10u32);
assert_eq!(current_era_info.era_index, 2u32);
assert_eq!(current_era_info.started_at, 10u32);

system_run_to_block(19);
run_to_block(20);
current_era_info = CurrentEraInfo::<Test>::get();
assert_eq!(current_era_info.current_era, 3u32);
assert_eq!(current_era_info.era_start, 20u32);
assert_eq!(current_era_info.era_index, 3u32);
assert_eq!(current_era_info.started_at, 20u32);
})
}
62 changes: 38 additions & 24 deletions pallets/capacity/src/tests/mock.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate as pallet_capacity;

use crate::{BalanceOf, Config, StakingRewardClaim, StakingRewardsProvider};
use common_primitives::{
node::{AccountId, Header, ProposalProvider},
node::{AccountId, Balance, Hash, Header, ProposalProvider},
schema::{SchemaId, SchemaValidator},
};
use frame_support::{
Expand All @@ -14,9 +15,6 @@ use sp_runtime::{
traits::{BlakeTwo256, Convert, IdentityLookup},
AccountId32, DispatchError, Perbill,
};
// use common_primitives::node::{Balance, Hash, RewardEra};
use common_primitives::node::RewardEra;
// use crate::{BalanceOf, StakingRewardClaim, StakingRewardsProvider};

type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
Expand Down Expand Up @@ -136,24 +134,40 @@ impl pallet_msa::Config for Test {
type MaxSignaturesStored = ConstU32<8000>;
}

// pub struct TestStakingRewardsProvider {}
// impl StakingRewardsProvider<Test> for TestStakingRewardsProvider {
// fn reward_pool_size(era: RewardEra) -> BalanceOf<Test> {
// Balance::from(10_000u64);
// }
//
// fn staking_reward_total(account_id: AccountId, from_era: RewardEra, to_era: RewardEra) -> BalanceOf<Test> {
// Balance::from(10u64)
// }
//
// fn validate_staking_reward_claim(account_id: AccountId, proof: Hash, payload: StakingRewardClaim<Test>) -> bool {
// true
// }
//
// fn payout_eligible(account_id: AccountId) -> bool {
// true
// }
// }
// not used yet
pub struct TestStakingRewardsProvider {}

type TestRewardEra = u32;

impl StakingRewardsProvider<Test> for TestStakingRewardsProvider {
type AccountId = u64;
type RewardEra = TestRewardEra;
type Hash = Hash; // use what's in common_primitives::node

fn reward_pool_size() -> Result<BalanceOf<Test>, DispatchError> {
Ok(1000u64)
}

fn staking_reward_total(
account_id: Self::AccountId,
_from_era: Self::RewardEra,
_to_era: Self::RewardEra,
) -> Result<BalanceOf<Test>, DispatchError> {
if account_id > 2u64 {
Ok(10u64)
} else {
Ok(1u64)
}
}

fn validate_staking_reward_claim(
_account_id: Self::AccountId,
_proof: Self::Hash,
_payload: StakingRewardClaim<Test>,
) -> bool {
true
}
}

// Needs parameter_types! for the Perbill
parameter_types! {
Expand All @@ -176,10 +190,10 @@ impl pallet_capacity::Config for Test {
type MaxEpochLength = ConstU32<100>;
type EpochNumber = u32;
type CapacityPerToken = TestCapacityPerToken;
type RewardEra = u32;
type RewardEra = TestRewardEra;
type EraLength = ConstU32<10>;
type StakingRewardsPastErasMax = ConstU32<5>;
// type RewardsProvider = TestStakingRewardsProvider;
type RewardsProvider = TestStakingRewardsProvider;
}

pub fn new_test_ext() -> sp_io::TestExternalities {
Expand Down
23 changes: 12 additions & 11 deletions pallets/capacity/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
pub mod capacity_details_tests;
pub mod epochs_tests;
mod capacity_details_tests;
mod epochs_tests;
mod eras_tests;
pub mod mock;
pub mod other_tests;
pub mod replenishment_tests;
pub mod stake_and_deposit_tests;
pub mod staking_account_details_tests;
pub mod staking_target_details_tests;
pub mod testing_utils;
pub mod unstaking_tests;
pub mod withdraw_unstaked_tests;
pub mod withdrawal_tests;
mod other_tests;
mod replenishment_tests;
mod rewards_provider_tests;
mod stake_and_deposit_tests;
mod staking_account_details_tests;
mod staking_target_details_tests;
mod testing_utils;
mod unstaking_tests;
mod withdraw_unstaked_tests;
mod withdrawal_tests;
Loading

0 comments on commit bbd773a

Please sign in to comment.