Skip to content

Commit

Permalink
feat(dapp-staking): Rework bonus rewards mechanism
Browse files Browse the repository at this point in the history
- Introduced move extrinsic for stake reallocation
- Implemented bonus status tracking with SafeMovesRemaining
- Configured logic for max bonus moves and forfeiture
- Added unit tests to validate bonus rewards functionality
- Added benchmarking and V8 to V9 migration logic
- Added comprehensive documentation for stake movement
- Updated bonus reward documentation with move conditions

Part of dApp Staking bonus rewards mechanism rework AstarNetwork#1379
  • Loading branch information
sylvaincormier committed Nov 24, 2024
1 parent 9640def commit 0e070a7
Show file tree
Hide file tree
Showing 20 changed files with 1,169 additions and 113 deletions.
38 changes: 26 additions & 12 deletions pallets/dapp-staking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ When `Voting` subperiod starts, all _stakes_ are reset to **zero**.
Projects participating in dApp staking are expected to market themselves to (re)attract stakers.

Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it.
Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate.
Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards, so they are encouraged to participate.

`Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting subperiod is treated as a single _voting era_.
E.g. if `voting` subperiod lasts for **5 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **500** blocks.
Expand Down Expand Up @@ -143,6 +143,25 @@ It is not possible to stake on a dApp that has been unregistered.
However, if dApp is unregistered after user has staked on it, user will keep earning
rewards for the staked amount.

#### Moving Stake Between Contracts

During a period, stakers have the ability to move their staked tokens between different contracts. This feature allows for greater flexibility in managing stakes without having to unstake and re-stake tokens. Here are the key points about stake movement:

* Each staker has a limited number of safe moves per period (`MaxBonusMovesPerPeriod`)
* Moving stake during the Build&Earn subperiod consumes one of these moves
* Moves during the Voting subperiod don't count against the move limit
* Once all safe moves are used, any additional moves will forfeit the bonus reward
* The target contract must be registered and active
* Cannot move stake between the same contract
* Cannot move more tokens than currently staked on the source contract
* Cannot move zero amount
* Move counter resets at period boundaries

This feature is particularly useful for:
* Adjusting strategy during a period without unstaking
* Responding to changes in dApp performance or status
* Optimizing reward potential while maintaining bonus eligibility

#### Unstaking Tokens

User can at any time decide to unstake staked tokens. There's no _unstaking_ process associated with this action.
Expand Down Expand Up @@ -177,6 +196,11 @@ Rewards are calculated using a simple formula: `staker_reward_pool * staker_stak

If staker staked on a dApp during the voting subperiod, and didn't reduce their staked amount below what was staked at the end of the voting subperiod, this makes them eligible for the bonus reward.

To maintain bonus reward eligibility when moving stake during the Build&Earn subperiod, stakers must:
* Have remaining safe moves available
* Not reduce their total staked amount below the voting period amount
* Only move stake between registered contracts

Bonus rewards need to be claimed per contract, unlike staker rewards.

Bonus reward is calculated using a simple formula: `bonus_reward_pool * staker_voting_subperiod_stake / total_voting_subperiod_stake`.
Expand Down Expand Up @@ -230,14 +254,4 @@ others will be left out. There is no strict rule which defines this behavior - i
having a larger stake than the other dApp(s). Tehnically, at the moment, the dApp with the lower `dApp Id` will have the advantage over a dApp with
the larger Id.

### Reward Expiry

Unclaimed rewards aren't kept indefinitely in storage. Eventually, they expire.
Stakers & developers should make sure they claim those rewards before this happens.

In case they don't, they will simply miss on the earnings.

However, this should not be a problem given how the system is designed.
There is no longer _stake&forger_ - users are expected to revisit dApp staking at least at the
beginning of each new period to pick out old or new dApps on which to stake on.
If they don't do that, they miss out on the bonus reward & won't earn staker rewards.
###
58 changes: 58 additions & 0 deletions pallets/dapp-staking/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,65 @@ mod benchmarks {

assert_last_event::<T>(Event::<T>::MaintenanceMode { enabled: true }.into());
}
#[benchmark]
fn move_stake() {
initial_config::<T>();

// Set up a staker account and required funds
let staker: T::AccountId = whitelisted_caller();
let amount = T::MinimumLockedAmount::get() * 2;
T::BenchmarkHelper::set_balance(&staker, amount);

// Register the source contract
let owner: T::AccountId = account("dapp_owner", 0, SEED);
let from_contract = T::BenchmarkHelper::get_smart_contract(1);
assert_ok!(DappStaking::<T>::register(
RawOrigin::Root.into(),
owner.clone().into(),
from_contract.clone(),
));

// Register the destination contract
let to_contract = T::BenchmarkHelper::get_smart_contract(2);
assert_ok!(DappStaking::<T>::register(
RawOrigin::Root.into(),
owner.clone().into(),
to_contract.clone(),
));

// Lock funds and stake on source contract
assert_ok!(DappStaking::<T>::lock(
RawOrigin::Signed(staker.clone()).into(),
amount,
));
assert_ok!(DappStaking::<T>::stake(
RawOrigin::Signed(staker.clone()).into(),
from_contract.clone(),
amount,
));

// Move to build and earn period to ensure move operation has worst case complexity
force_advance_to_next_subperiod::<T>();
assert_eq!(
ActiveProtocolState::<T>::get().subperiod(),
Subperiod::BuildAndEarn,
"Sanity check - we need to be in build&earn period."
);

let move_amount = amount / 2;

#[extrinsic_call]
_(
RawOrigin::Signed(staker.clone()),
from_contract.clone(),
to_contract.clone(),
move_amount,
);

// Verify that an event was emitted
let last_events = dapp_staking_events::<T>();
assert!(!last_events.is_empty(), "No events found");
}
#[benchmark]
fn register() {
initial_config::<T>();
Expand Down
Loading

0 comments on commit 0e070a7

Please sign in to comment.