Skip to content

Commit

Permalink
Feat/separate unlock chunks #1753 (#1763)
Browse files Browse the repository at this point in the history
 separate unstaking unlock chunks into their own storage and do a storage migration which includes staking type, for
convenience of the planned Provider Boost feature so that another storage migration is not needed for that feature.
  • Loading branch information
shannonwells authored Dec 1, 2023
1 parent 9691055 commit afa4490
Show file tree
Hide file tree
Showing 17 changed files with 638 additions and 405 deletions.
4 changes: 2 additions & 2 deletions designdocs/capacity.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ Storage for keeping records of staking accounting.
/// Storage for keeping a ledger of staked token amounts for accounts.
#[pallet::storage]
pub type StakingAccountLedger<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, StakingAccountDetails<T::Balance>>;
StorageMap<_, Twox64Concat, T::AccountId, StakingDetails<T::Balance>>;

```

Expand Down Expand Up @@ -355,7 +355,7 @@ The type used for storing information about staking details.

```rust

pub struct StakingAccountDetails<Balance, BlockNumber> {
pub struct StakingDetails<Balance, BlockNumber> {
/// The amount a Staker has staked, minus the sum of all tokens in `unlocking`.
pub active: Balance,
/// The total amount of tokens in `active` and `unlocking`
Expand Down
35 changes: 15 additions & 20 deletions designdocs/capacity_staking_rewards_implementation.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
# Capacity Staking Rewards Implementation

## Overview
Staking Capacity for rewards is a new feature which allows token holders to stake FRQCY and split the staking
rewards with a Provider they choose. The Provider receives a small reward in Capacity
(which is periodically replenished), and the staker receives a periodic return in FRQCY token.
The amount of Capacity that the Provider would receive in such case is a fraction of what they would get from a
`MaximumCapacity` stake.
This document describes a new type of staking which allows token holders to stake FRQCY and split staking rewards with a Provider the staker chooses.

Currently, when staking token for Capacity, the only choice is to assign all the generated Capacity to the designated target.
The target, who must be a Provider, may then spend this Capacity to pay for specific transactions. This is called **Maximized Capacity** staking.

In this new type of staking, called **Provider Boosting**, the Provider receives a reward in Capacity and the staker receives a periodic return in FRQCY token.
The amount of Capacity that the Provider would receive in such case is a less than what they would get from a `MaximumCapacity` stake.

The period of Capacity replenishment - the `Epoch` - and the period of token reward - the `RewardEra`- are different.
Epochs much necessarily be much shorter than rewards because Capacity replenishment needs to be multiple times a day to meet the needs of a high traffic network, and to allow Providers the ability to delay transactions to a time of day with lower network activity if necessary.
Reward eras need to be on a much longer scale, such as every two weeks, because there are potentially orders of magnitude more stakers, and calculating rewards is computationally more intensive than updating Capacity balances for the comparatively few Providers.
In addition, this lets the chain to store Reward history for much longer rather than forcing people to have to take steps to claim rewards.

### Diagram
This illustrates roughly (and not to scale) how Provider Boost staking works. Just like the current staking behavior, now called Maximized staking, The Capacity generated by staking is added to the Provider's Capacity ledger immediately so it can be used right away. The amount staked is locked in Alice's account, preventing transfer.
This illustrates roughly -- not to scale and **NOT reflecting actual reward amounts** -- how Provider Boost staking is expected to work. Just like the current staking behavior, now called Maximium staking, The Capacity generated by staking is added to the Provider's Capacity ledger immediately so it can be used right away. The amount staked is locked in Alice's account, preventing transfer.

Provider Boost token rewards are earned only for token staked for a complete Reward Era. So Alice does not begin earning rewards until Reward Era 5 in the diagram, and this means Alice must wait until Reward Era 6 to claim rewards for Reward Era 5. Unclaimed reward amounts are actually not minted or transferred until they are claimed, and may also not be calculated until then, depending on the economic model.

This process will be described in more detail in the Economic Model Design Document.

### NOTE: Actual reward amounts are TBD; amounts are for illustration purposes only
![Provider boosted staking](https://github.com/LibertyDSNP/frequency/assets/502640/ffb632f2-79c2-4a09-a906-e4de02e4f348)

The proposed feature is a design for staking FRQCY token in exchange for Capacity and/or FRQCY.
Expand Down Expand Up @@ -50,23 +53,15 @@ It does not give regard to what the economic model actually is, since that is ye

## Staking Token Rewards

### StakingAccountDetails updates
### StakingAccountDetails --> StakingDetails
New fields are added. The field **`last_rewarded_at`** is to keep track of the last time rewards were claimed for this Staking Account.
MaximumCapacity staking accounts MUST always have the value `None` for `last_rewarded_at`.
Finally, `stake_change_unlocking`, is added, which stores an `UnlockChunk` when a staking account has changed.
targets for some amount of funds. This is to prevent retarget spamming.

This will be a V2 of this storage and original StakingAccountDetails will need to be migrated.
This is a second version of this storage, to replace StakingAccountDetails, and StakingAccountDetails data will need to be migrated.
```rust
pub struct StakingAccountDetailsV2<T: Config> {
pub struct StakingDetails<T: Config> {
pub active: BalanceOf<T>,
pub total: BalanceOf<T>,
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>, T::EpochNumber>, T::MaxUnlockingChunks>,
/// The number of the last StakingEra that this account's rewards were claimed.
pub last_rewards_claimed_at: Option<T::RewardEra>, // NEW None means never rewarded, Some(RewardEra) means last rewarded RewardEra.
/// staking amounts that have been retargeted are prevented from being retargeted again for the
/// configured Thawing Period number of blocks.
pub stake_change_unlocking: BoundedVec<UnlockChunk<BalanceOf<T>, T::RewardEra>, T::MaxUnlockingChunks>, // NEW
}
```

Expand Down Expand Up @@ -150,7 +145,7 @@ pub struct StakingRewardClaim<T: Config> {
/// How much is claimed, in token
pub claimed_reward: Balance,
/// The end state of the staking account if the operations are valid
pub staking_account_end_state: StakingAccountDetails,
pub staking_account_end_state: StakingDetails,
/// The starting era for the claimed reward period, inclusive
pub from_era: T::RewardEra,
/// The ending era for the claimed reward period, inclusive
Expand Down Expand Up @@ -264,7 +259,7 @@ calculate rewards on chain at all.

Regardless, on success, the claimed rewards are minted and transferred as locked token to the origin, with the existing
unstaking thaw period for withdrawal (which simply unlocks thawed token amounts as before).
There is no chunk added; instead the existing unstaking thaw period is applied to last_rewards_claimed_at in StakingAccountDetails.
There is no chunk added; instead the existing unstaking thaw period is applied to last_rewards_claimed_at in StakingDetails.

Forcing stakers to wait a thaw period for every claim is an incentive to claim rewards sooner than later, leveling out
possible inflationary effects and helping prevent unclaimed rewards from expiring.
Expand Down Expand Up @@ -336,7 +331,7 @@ No more than `T::MaxUnlockingChunks` staking amounts may be retargeted within th
Each call creates one chunk. Emits a `StakingTargetChanged` event with the parameters of the extrinsic.
```rust
/// Sets the target of the staking capacity to a new target.
/// This adds a chunk to `StakingAccountDetails.stake_change_unlocking chunks`, up to `T::MaxUnlockingChunks`.
/// This adds a chunk to `StakingDetails.stake_change_unlocking chunks`, up to `T::MaxUnlockingChunks`.
/// The staked amount and Capacity generated by `amount` originally targeted to the `from` MSA Id is reassigned to the `to` MSA Id.
/// Does not affect unstaking process or additional stake amounts.
/// Changing a staking target to a Provider when Origin has nothing staked them will retain the staking type.
Expand Down
34 changes: 21 additions & 13 deletions pallets/capacity/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::*;
use crate::Pallet as Capacity;

use frame_benchmarking::{account, benchmarks, whitelist_account};
use frame_support::{assert_ok, traits::Currency};
use frame_support::{assert_ok, traits::Currency, BoundedVec};
use frame_system::RawOrigin;
use parity_scale_codec::alloc::vec::Vec;

Expand Down Expand Up @@ -56,21 +56,19 @@ benchmarks! {

withdraw_unstaked {
let caller: T::AccountId = create_funded_account::<T>("account", SEED, 5u32);
let amount: BalanceOf<T> = T::MinimumStakingAmount::get();

let mut staking_account = StakingAccountDetails::<T>::default();
staking_account.deposit(500u32.into());

// set new unlock chunks using tuples of (value, thaw_at)
let new_unlocks: Vec<(u32, u32)> = Vec::from([(50u32, 3u32), (50u32, 5u32)]);
assert_eq!(true, staking_account.set_unlock_chunks(&new_unlocks));
let mut unlocking: UnlockChunkList<T> = BoundedVec::default();
for _i in 0..T::MaxUnlockingChunks::get() {
let unlock_chunk: UnlockChunk<BalanceOf<T>, T::EpochNumber> = UnlockChunk { value: 1u32.into(), thaw_at: 3u32.into() };
assert_ok!(unlocking.try_push(unlock_chunk));
}
UnstakeUnlocks::<T>::set(&caller, Some(unlocking));

Capacity::<T>::set_staking_account(&caller.clone(), &staking_account);
CurrentEpoch::<T>::set(T::EpochNumber::from(5u32));

}: _ (RawOrigin::Signed(caller.clone()))
verify {
assert_last_event::<T>(Event::<T>::StakeWithdrawn {account: caller, amount: 100u32.into() }.into());
let total = T::MaxUnlockingChunks::get();
assert_last_event::<T>(Event::<T>::StakeWithdrawn {account: caller, amount: total.into() }.into());
}

on_initialize {
Expand All @@ -91,18 +89,28 @@ benchmarks! {
let target = 1;
let block_number = 4u32;

let mut staking_account = StakingAccountDetails::<T>::default();
let mut staking_account = StakingDetails::<T>::default();
let mut target_details = StakingTargetDetails::<BalanceOf<T>>::default();
let mut capacity_details = CapacityDetails::<BalanceOf<T>, <T as Config>::EpochNumber>::default();

staking_account.deposit(staking_amount);
target_details.deposit(staking_amount, capacity_amount);
capacity_details.deposit(&staking_amount, &capacity_amount);

Capacity::<T>::set_staking_account(&caller.clone(), &staking_account);
let _ = Capacity::<T>::set_staking_account_and_lock(&caller.clone(), &staking_account);
Capacity::<T>::set_target_details_for(&caller.clone(), target, target_details);
Capacity::<T>::set_capacity_for(target, capacity_details);

// fill up unlock chunks to max bound - 1
let count = T::MaxUnlockingChunks::get()-1;
let mut unlocking: UnlockChunkList<T> = BoundedVec::default();
for _i in 0..count {
let unlock_chunk: UnlockChunk<BalanceOf<T>, T::EpochNumber> = UnlockChunk { value: 1u32.into(), thaw_at: 3u32.into() };
assert_ok!(unlocking.try_push(unlock_chunk));
}
UnstakeUnlocks::<T>::set(&caller, Some(unlocking));


}: _ (RawOrigin::Signed(caller.clone()), target, unstaking_amount.into())
verify {
assert_last_event::<T>(Event::<T>::UnStaked {account: caller, target: target, amount: unstaking_amount.into(), capacity: Capacity::<T>::calculate_capacity_reduction(unstaking_amount.into(), staking_amount, capacity_amount) }.into());
Expand Down
Loading

0 comments on commit afa4490

Please sign in to comment.