Skip to content

Commit

Permalink
Implement rewards calculation formula #1941 (#1956)
Browse files Browse the repository at this point in the history
The goal of this PR is to implement (but not really use yet) the chosen
formula for calculation of a reward in a single Provider Boost Reward
Era.

Closes #1941
  • Loading branch information
shannonwells committed May 21, 2024
1 parent d9fce3b commit 7e0f391
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 87 deletions.
4 changes: 2 additions & 2 deletions common/primitives/src/capacity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ impl TargetValidator for () {
}
}

/// A trait for Non-transferable asset.
/// A trait for Non-transferable asset
pub trait Nontransferable {
/// Scalar type for representing staked token and capacity balances.
/// Scalar type for representing balance of an account.
type Balance: Balance;

/// The balance Capacity for an MSA.
Expand Down
3 changes: 0 additions & 3 deletions common/primitives/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ pub type Signature = MultiSignature;
/// Index of a transaction in the chain.
pub type Index = u32;

/// 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.
pub type Hash = sp_core::H256;
/// The provider of a collective action interface, for example an instance of `pallet-collective`.
Expand Down
7 changes: 6 additions & 1 deletion designdocs/provider_boosting_implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,12 @@ pub trait StakingRewardsProvider<T: Config> {
/// Returns whether the claim passes validation. Accounts must first pass `payoutEligible` test.
/// Errors: None
fn validate_staking_reward_claim(account_id: T::AccountID, proof: Hash, payload: StakingRewardClaim<T>) -> bool;
}

/// Calculate a reward for a single era based on a chosen economic model
fn era_staking_reward(amount_staked: Self::Balance,
total_staked: Self::Balance,
reward_pool_size: Self::Balance,
) -> Self::Balance}
```

### NEW: Config items
Expand Down
49 changes: 34 additions & 15 deletions pallets/capacity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ use frame_support::{

use sp_runtime::{
traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero},
ArithmeticError, DispatchError, Perbill,
ArithmeticError, DispatchError, Perbill, Permill,
};

pub use common_primitives::{
Expand Down Expand Up @@ -203,6 +203,14 @@ pub mod pallet {
/// A staker may not retarget more than MaxRetargetsPerRewardEra
#[pallet::constant]
type MaxRetargetsPerRewardEra: Get<u32>;

/// The fixed size of the reward pool in each Reward Era.
#[pallet::constant]
type RewardPoolEachEra: Get<BalanceOf<Self>>;

/// the percentage cap per era of an individual Provider Boost reward
#[pallet::constant]
type RewardPercentCap: Get<Permill>;
}

/// Storage for keeping a ledger of staked token amounts for accounts.
Expand Down Expand Up @@ -1111,24 +1119,20 @@ impl<T: Config> StakingRewardsProvider<T> for Pallet<T> {
type AccountId = T::AccountId;
type RewardEra = T::RewardEra;
type Hash = T::Hash;
type Balance = BalanceOf<T>;

// 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
fn reward_pool_size(total_staked: BalanceOf<T>) -> BalanceOf<T> {
if total_staked.is_zero() {
return BalanceOf::<T>::zero()
}

// For now reward pool size is set to 10% of total staked token
total_staked.checked_div(&BalanceOf::<T>::from(10u8)).unwrap_or_default()
fn reward_pool_size(_total_staked: Self::Balance) -> Self::Balance {
T::RewardPoolEachEra::get()
}

// Performs range checks plus a reward calculation based on economic model for the era range
fn staking_reward_total(
_account_id: T::AccountId,
from_era: T::RewardEra,
to_era: T::RewardEra,
) -> Result<BalanceOf<T>, DispatchError> {
_account_id: Self::AccountId,
from_era: Self::RewardEra,
to_era: Self::RewardEra,
) -> Result<Self::Balance, DispatchError> {
let era_range = from_era.saturating_sub(to_era);
ensure!(
era_range.le(&T::StakingRewardsPastErasMax::get().into()),
Expand All @@ -1146,16 +1150,31 @@ impl<T: Config> StakingRewardsProvider<T> for Pallet<T> {
Ok(per_era.saturating_mul(num_eras.into()))
}

/// Calculate the staking reward for a single era. We don't care about the era number,
/// just the values.
fn era_staking_reward(
era_amount_staked: Self::Balance,
era_total_staked: Self::Balance,
era_reward_pool_size: Self::Balance,
) -> Self::Balance {
let capped_reward = T::RewardPercentCap::get().mul(era_amount_staked);
let proportional_reward = era_reward_pool_size
.saturating_mul(era_amount_staked)
.checked_div(&era_total_staked)
.unwrap_or_else(|| Zero::zero());
proportional_reward.min(capped_reward)
}

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

/// How much, as a percentage of staked token, to boost a targeted Provider when staking.
fn capacity_boost(amount: BalanceOf<T>) -> BalanceOf<T> {
fn capacity_boost(amount: Self::Balance) -> Self::Balance {
Perbill::from_percent(50u32).mul(amount)
}
}
53 changes: 32 additions & 21 deletions pallets/capacity/src/tests/change_staking_target_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ fn do_retarget_happy_path() {
assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &to_amount));

// expect from stake amounts to be halved
assert_capacity_details(from_msa, 1, 300, 1);
assert_capacity_details(from_msa, 15, 300, 15);

// expect to stake amounts to be increased by the retarget amount
assert_capacity_details(to_msa, 3, 600, 3);
assert_capacity_details(to_msa, 30, 600, 30);

assert_target_details(staker, from_msa, 300, 1);
assert_target_details(staker, from_msa, 300, 15);

assert_target_details(staker, to_msa, 600, 3);
assert_target_details(staker, to_msa, 600, 30);
})
}

Expand All @@ -77,8 +77,8 @@ fn do_retarget_flip_flop() {
assert_ok!(Capacity::do_retarget(&staker, &to_msa, &from_msa, &to_amount,));
}
}
assert_capacity_details(from_msa, 3, 600, 3);
assert_capacity_details(to_msa, 1, 300, 1);
assert_capacity_details(from_msa, 30, 600, 30);
assert_capacity_details(to_msa, 15, 300, 15);
})
}

Expand All @@ -94,18 +94,18 @@ fn check_retarget_rounding_errors() {

setup_provider(&staker, &from_msa, &from_amount, ProviderBoost);
setup_provider(&staker, &to_msa, &to_amount, ProviderBoost);
assert_capacity_details(from_msa, 3, 666, 3);
assert_capacity_details(to_msa, 1, 301, 1);
assert_capacity_details(from_msa, 33, 666, 33);
assert_capacity_details(to_msa, 15, 301, 15);
// 666+301= 967, 3+1=4

assert_ok!(Capacity::do_retarget(&staker, &from_msa, &to_msa, &301u64));
assert_capacity_details(to_msa, 3, 602, 3);
assert_capacity_details(from_msa, 1, 365, 1);
assert_capacity_details(from_msa, 18, 365, 18);
assert_capacity_details(to_msa, 30, 602, 30);
// 602+365 = 967, 3+1 = 4

assert_ok!(Capacity::do_retarget(&staker, &to_msa, &from_msa, &151u64));
assert_capacity_details(to_msa, 2, 451, 2);
assert_capacity_details(from_msa, 2, 516, 2);
assert_capacity_details(from_msa, 26, 516, 26);
assert_capacity_details(to_msa, 22, 451, 22);
// 451+516 = 967, 2+2 = 4
})
}
Expand All @@ -121,6 +121,7 @@ fn assert_total_capacity(msas: Vec<MessageSourceId>, total: u64) {
assert_eq!(total, sum);
}

// Tests that the total stake remains the same after retargets
#[test]
fn check_retarget_multiple_stakers() {
new_test_ext().execute_with(|| {
Expand All @@ -136,21 +137,31 @@ fn check_retarget_multiple_stakers() {

setup_provider(&staker_10k, &from_msa, &647u64, ProviderBoost);
setup_provider(&staker_500, &to_msa, &293u64, ProviderBoost);
assert_ok!(Capacity::stake(RuntimeOrigin::signed(staker_600.clone()), from_msa, 479u64,));
assert_ok!(Capacity::stake(RuntimeOrigin::signed(staker_400.clone()), to_msa, 211u64,));
assert_ok!(Capacity::provider_boost(
RuntimeOrigin::signed(staker_600.clone()),
from_msa,
479u64,
));
assert_ok!(Capacity::provider_boost(
RuntimeOrigin::signed(staker_400.clone()),
to_msa,
211u64,
));

// 647 * .10 * .5 = 32 (rounded)
// 293 * .10 * .5 = 15 (rounded)
// 479 * .10 * .5 = 24 (round)
// 211 * .10 * .5 = 10 (rounded down)
// total capacity should be sum of above
let expected_total = 81u64;

// 647 * .1 * .05 = 3 (rounded down)
// 293 * .1 * .05 = 1 (rounded down)
// 479 * .1 = 48 (rounded up)
// 211 * .1 = 21 (rounded down)
// total capacity should be 73
assert_total_capacity(vec![from_msa, to_msa], 73);
assert_total_capacity(vec![from_msa, to_msa], expected_total);

assert_ok!(Capacity::do_retarget(&staker_10k, &from_msa, &to_msa, &amt2));
assert_ok!(Capacity::do_retarget(&staker_600, &from_msa, &to_msa, &amt1));
assert_ok!(Capacity::do_retarget(&staker_500, &to_msa, &from_msa, &amt1));
assert_ok!(Capacity::do_retarget(&staker_400, &to_msa, &from_msa, &amt1));
assert_total_capacity(vec![from_msa, to_msa], 73);
assert_total_capacity(vec![from_msa, to_msa], expected_total);
})
}

Expand Down
25 changes: 19 additions & 6 deletions pallets/capacity/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use frame_system::EnsureSigned;
use sp_core::{ConstU8, H256};
use sp_runtime::{
traits::{BlakeTwo256, Convert, IdentityLookup},
AccountId32, BuildStorage, DispatchError, Perbill,
AccountId32, BuildStorage, DispatchError, Perbill, Permill,
};
use sp_std::ops::Mul;

Expand Down Expand Up @@ -142,24 +142,34 @@ impl StakingRewardsProvider<Test> for TestStakingRewardsProvider {
type AccountId = u64;
type RewardEra = TestRewardEra;
type Hash = Hash; // use what's in common_primitives::node
type Balance = BalanceOf<Test>;

// To reflect new economic model behavior of having a constant RewardPool amount.
fn reward_pool_size(_total_staked: BalanceOf<Test>) -> BalanceOf<Test> {
fn reward_pool_size(_total_staked: Self::Balance) -> Self::Balance {
10_000u64.into()
}

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

// use the pallet version of the era calculation.
fn era_staking_reward(
amount_staked: Self::Balance,
total_staked: Self::Balance,
reward_pool_size: Self::Balance,
) -> Self::Balance {
Capacity::era_staking_reward(amount_staked, total_staked, reward_pool_size)
}

fn validate_staking_reward_claim(
_account_id: Self::AccountId,
_proof: Self::Hash,
Expand All @@ -168,14 +178,15 @@ impl StakingRewardsProvider<Test> for TestStakingRewardsProvider {
true
}

fn capacity_boost(amount: BalanceOf<Test>) -> BalanceOf<Test> {
Perbill::from_percent(5u32).mul(amount)
fn capacity_boost(amount: Self::Balance) -> Self::Balance {
Perbill::from_percent(50u32).mul(amount)
}
}

// Needs parameter_types! for the Perbill
parameter_types! {
pub const TestCapacityPerToken: Perbill = Perbill::from_percent(10);
pub const TestRewardCap: Permill = Permill::from_parts(3_800); // 0.38% or 0.0038 per RewardEra
}
impl pallet_capacity::Config for Test {
type RuntimeEvent = RuntimeEvent;
Expand All @@ -198,8 +209,10 @@ impl pallet_capacity::Config for Test {
type RewardEra = TestRewardEra;
type EraLength = ConstU32<10>;
type StakingRewardsPastErasMax = ConstU32<5>;
type RewardsProvider = TestStakingRewardsProvider;
type RewardsProvider = Capacity;
type MaxRetargetsPerRewardEra = ConstU32<5>;
type RewardPoolEachEra = ConstU64<10_000>;
type RewardPercentCap = TestRewardCap;
}

pub fn new_test_ext() -> sp_io::TestExternalities {
Expand Down
4 changes: 2 additions & 2 deletions pallets/capacity/src/tests/provider_boost_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fn provider_boost_works() {
let account = 600;
let target: MessageSourceId = 1;
let amount = 200;
let capacity = 1;
let capacity = 10; // Maximized stake (10% of staked amount) * 50% (in trait impl)
register_provider(target, String::from("Foo"));
assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(account), target, amount));

Expand All @@ -23,7 +23,7 @@ fn provider_boost_works() {

// Check that the capacity generated is correct. (5% of amount staked, since 10% is what's in the mock)
let capacity_details = Capacity::get_capacity_for(target).unwrap();
assert_eq!(capacity_details.total_capacity_issued, 1u64);
assert_eq!(capacity_details.total_capacity_issued, capacity);

let events = staking_events();
assert_eq!(
Expand Down
Loading

0 comments on commit 7e0f391

Please sign in to comment.