Skip to content

Commit ac1064e

Browse files
Gunit2481Dinonard
andauthored
Add V3 Technical Architecture and Design doc to the build section (#590)
* Add V3 Technical Architecture and Design to the build section * Refactor part(1) * Full update * Update docs/build/dapp-staking/dapp_staking_architecture.md Co-authored-by: Gaius_sama <85451570+Gunit2481@users.noreply.github.com> * Update docs/build/dapp-staking/dapp_staking_architecture.md Co-authored-by: Gaius_sama <85451570+Gunit2481@users.noreply.github.com> * Update docs/build/dapp-staking/dapp_staking_architecture.md Co-authored-by: Gaius_sama <85451570+Gunit2481@users.noreply.github.com> * Update dapp_staking_architecture.md * Update dapp_staking_architecture.md --------- Co-authored-by: Dino Pacandi <dino.pacandi@gmail.com> Co-authored-by: Dino Pačandi <3002868+Dinonard@users.noreply.github.com>
1 parent 7a2b8d5 commit ac1064e

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"label": "dApp Staking",
3+
"position": 5
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
---
2+
sidebar_position: 1
3+
title: Technical Solution
4+
---
5+
6+
:::important
7+
The target audience for this page are developers building projects which interact with dApp Staking protocol.
8+
:::
9+
10+
Please make sure to check the [existing](/docs/learn/dapp-staking/dapp-staking-protocol) dApp staking protocol documentation before diving into this document.
11+
12+
## Pallet Internals
13+
14+
To avoid duplicating information, please check the code documentation for respective crates/modules:
15+
16+
* [dApp Staking pallet](https://github.com/AstarNetwork/Astar/tree/master/pallets/dapp-staking-v3)
17+
* [dApp Staking precompile](https://github.com/AstarNetwork/Astar/tree/master/precompiles/dapp-staking-v3)
18+
* [inflation pallet](https://github.com/AstarNetwork/Astar/tree/master/pallets/inflation)
19+
20+
At the moment of writing this document, _crate pages_ aren't hosted anywhere, but you can build them locally like:
21+
22+
```bash
23+
cargo doc --open --no-deps -p pallet-dapp-staking-v3
24+
```
25+
26+
_Make sure to replace package name with whatever package you're interested in._
27+
28+
The generated documentation will contain exhaustive description of pallet extrinsic calls, types, evens, errors, storage items, examples, and more.
29+
30+
## Scenarios
31+
32+
The following subchapters are aimed to help users understand the logic behind some internal workings of the pallet.
33+
This information is intended to be complimentary to the aforementioned pallet documentation.
34+
35+
The `CurrentProtocolState` storage entry is relevant essentially to every functionality, so it won't be repeated in every subchapter.
36+
37+
### Staked Amounts In Ledger
38+
39+
The `AccountLedger` struct contains various pieces of information related to someone's locked & staked amounts.
40+
For each staker, an entry of `AccountLedger` is stored in `Ledger` storage map.
41+
42+
Both `staked` and `staked_future` fields carry information about how much user has staked at some point.
43+
If `staked_future` is not `None` (or `null`), then it’s guaranteed to have `era` value equal to `staked.era + 1`.
44+
Each of these entries caries information about certain era or time span.
45+
46+
There are 4 distinct scenarios how these values can appear:
47+
48+
1. `staked` is empty (all zeroes), and `staked_future` is `None`. This means the account has nothing staked.
49+
50+
2. `staked` is non-empty and `staked_future` is `None`. This can be read as: "Staker has staked `staked.voting + staked.build_and_earn` amount since era `staked.era`".
51+
E.g., if `staked.era = 5` and current era is 7, it means that the `staked` entry is valid for eras **5, 6 and 7** (assuming they all belong to the same period).
52+
53+
3. `staked` is empty (all zeroes), and `staked_future` has some non-zero value. This is interpreted in the same way as the staked value in the previous example.
54+
55+
4. `staked` is non-empty, and `staked_future` has some non-zero value. In this case, `staked` describes **a single era**, while `staked_future` describes one or more eras.
56+
E.g. if `staked.era = 5` , and `staked_future.era = 6` it’s interpreted as:
57+
* In era 5, staker has staked `staked.voting + staked.build_and_earn` amount.
58+
* From era 6 and onwards, staker has staked `staked_future.voting + staked_future.build_and_earn`
59+
60+
`stake` and `staked_future` entries are not valid indefinitely, they will expire after the period finishes. However, to expire doesn’t mean they are deleted or anything
61+
similar to that. Instead, `staked.period` or `staked_future.period` need to be checked to understand whether they match the ongoing period number.
62+
If they don’t match, they can be ignored & treated as if stake amount is **zero**.
63+
64+
E.g. if `staked.era = 5` and `staked.period = 1`, and current period is `2`, we need to check `PeriodEnd` storage map to find out when did `period 1` end.
65+
Let's assume that `period 1` ended in era 20 - it would mean that the `staked` entry is valid from era 5 up to era 20.
66+
67+
### Understanding Claimable Eras For Stakers
68+
69+
There are two storage entries to consider `Ledger` (`AccountLedger` struct) and `PeriodEnd` (`PeriodEndInfo` struct).
70+
71+
The relevant entries for the `AccountLedger` are `staked` and `staked_future`.
72+
In case `staked_future` is not `None` (or not `null`), then its era **must** be exactly `+1` compared to the `staked` era.
73+
E.g. if `staked.era = 15` , then `staked_future.era`, if it exists, must be `16`.
74+
75+
First step in getting claimable staker reward eras to find the final era for which rewards can be claimed. There are three possibilities:
76+
1. Rewards have expired and there’s nothing to claim.
77+
2. Rewards are from a past period (`staker.period` or `staker_future.period` is older than the ongoing period) in which case `PeriodEnd` storage entry should be read to find the ending era of that period.
78+
3. Rewards are from the ongoing period in which case ending era is `protocol_state.current_era - 1`
79+
80+
Once we have the _latest_ era for which the rewards can be claimed, we can construct a list of claimable eras with their appropriate stake amount.
81+
82+
There are a few options:
83+
_(please note that `.amount` notation is just a simplification for `staked.voting + staked.build_and_earn` sum)_
84+
85+
1. Only `staked` exists, and `staked_future` is `None`. Vector of stake entries looks like `[(staked.era, staked.amount), (staked.era + 1, staked.amount), ..., (final_era, staked.amount)]`
86+
87+
2. Only `staked_future` exists, and `staked` only has zero entries. Vector of stake entries looks like `[(staked_future.era, staked.amount), (staked_future.era + 1, staked_future.amount), ..., (final_era, staked_future.amount)]`
88+
89+
3. Both `staked` and `staked_future` are non-zero. Vector of stake entries looks like `[(staked.era, staked.amount), (staked_future.era, staked_future.amount), ..., (final_era, staked_future.amount)]`
90+
91+
### Number of Staker Claim Calls Required To Claim All Rewards
92+
93+
Stakers can have many pending rewards if they don't claim regularly. This is normal and expected.
94+
One `claim_staker_rewards` call can claim more than 1 such reward.
95+
96+
To calculate number of calls required to claim **all** rewards, we need to to the following:
97+
98+
1. Repeat the step from the subchapter which explains how to get list of claimable eras.
99+
100+
2. Information about era rewards is stored inside spans - `EraRewards` storage map & `EraRewardSpan` struct.
101+
Span length is defined by a runtime constant `EraRewardSpanLength`.
102+
103+
Once we know claimable eras, we need to take the first and last era and put it into the following calculation:
104+
105+
```rust
106+
let first_span_index = (first_era - (first_era % EraRewardSpanLength)) / EraRewardSpanLength;
107+
let last_span_index = (last_era - (last_era % EraRewardSpanLength)) / EraRewardSpanLength;
108+
109+
let number_of_claims = last_span_index - first_span_index + 1;
110+
```
111+
112+
:::note
113+
This code assumes that there _are_ claimable rewards. if the logic for getting first and/or last era is wrong (e.g. returns a number when there’s nothing to claim), the above formula won’t work.
114+
:::
115+
116+
### Staker Reward Calculation
117+
118+
Pending staker reward for a concrete era can be calculated using a simple formula.
119+
120+
1. Find out the `total staked amount` a staker had in some `era`. See previous chapters on how to extract this information from the `Ledger` storage map.
121+
2. Find out how much was staked in total at the end of an `era`, and what the reward pool was. This can be read from the `EraRewards` storage map.
122+
123+
`reward = total_staked_amount / era_reward.staked * era_reward.staker_reward_pool`
124+
125+
:::note
126+
Developer should take note of the order of operations above to prevent overflow/underflow. Due to both **ASTR** and **SDN** currency having 18 decimals, _BigInteger_ usage is encouraged.
127+
:::
128+
129+
### dApp Reward Claiming
130+
131+
In order to claim dApp rewards, it is necessary to know **exactly** which eras have unclaimed reward for the dApp.
132+
The relevant storage map is `DAppTiers` which maps `era` to information about dApp tiers & tier rewards.
133+
134+
After reading `DappTiers` storage map for a particular `era`, `dapp_tiers_rewards.dapps` _tree map_ must be checked whether it contains the `dapp_id` of the smart contract for which we want to claim rewards. Please note that `dapp_id` is `u16` dApp identifier which can be read from the `DAppInfo` struct in `IntegratedDAppsStorage`.
135+
136+
In case entry for the `dapp_id` exists, it will also contain the `tier_id` value which can be used to read the earned dApp reward from `dapp_tier_info.rewards`.
137+
It’s enough to use `tier_id` it as index in the `rewards` vector to find the reward associated with that tier.
138+
139+
Once reward has been claimed, the associated entry will be removed `dapp_tiers_rewards.dapps` _tree map_.
140+
141+
### Reward Expiry
142+
143+
After predefined amount of periods have passed, unclaimed rewards will expire.
144+
This means that staker or dApp owner won't be able to claim these anymore.
145+
146+
The oldest period for which the rewards can be claimed can be calculated like this:
147+
148+
`oldest_period = current_period - RewardRetentionInPeriods`
149+
150+
where `RewardRetentionInPeriods` is a runtime constant.
151+
152+
This can be used to limit the _iteration_ over `DappTiers` storage.
153+
154+
Once we know the oldest period, we can use `PeriodEnd` storage map to find when did the `oldest_period - 1` period end. The era after that one (or +2 to be more precise since +1 refers to the voting subperiod era) will be the first one that has the potential to be claimable.
155+
156+
### Bonus Rewards
157+
158+
When checking whether staker is eligible for any bonus rewards, it is necessary to check all of the `StakerInfo` double storage map entries related to that staker.
159+
The first key of the double map is `staker account` so it can easily be iterated via prefix iteration.
160+
161+
If the `staked` field of the `SingularStakingInfo` refers to a valid **past period** (non-expired), and `loyal_staker` flag is set to `true`, it means staker is eligible for the bonus reward.
162+
163+
It's also required to read the `PeriodEnd` storage map for information about the finished period.
164+
165+
The reward can be calculated as:
166+
167+
`bonus_reward = singular_staking_info.stake_amount.voting / period_end_info.total_vp_stake * period_end_info.bonus_reward_pool`
168+
169+
Once reward has been claimed, the database entry will be cleaned up.
170+
171+
### Understanding Tier Rewards
172+
173+
At the end of each `Build&Earn` subperiod era, dApp scores are calculated, and according to them, dApps are assigned into tiers.
174+
175+
Each tier has a limited capacity, and has a threshold which dApps need to satisfy in order to enter it.
176+
The dApp score is simply the total staked amount on the dApp (value can be read from `ContractStake` storage map).
177+
Tiers are described using `TierConfiguration` struct which is stored in `TierConfig` storage.
178+
Using that information, dApps are sorted out and assigned into appropriate tiers.
179+
180+
Once tiers have been assigned, they are stored into `DAppTiers` storage map. This is done at the end of every `Build&Earn` subperiod era, or at the beginning of the block of the next era to be more precise.
181+
182+
Essentially, it is enough to check that storage item once it’s been written to understand how many tier slots have been occupied and how many are unused.
183+
184+
However, it is possible that in that very same block, someone calls `claim_dapp_reward` extrinsic. This will remove some of the entries from the storage, thus not providing the correct picture of tier usage. To build 100% accurate picture of how many slots were occupied:
185+
186+
1. Find the block at which new era started, when dApps were assigned to tiers.
187+
2. Use Runtime Call API to get the tier assignment for the **previous block**
188+
189+
### Reward Pools
190+
191+
Reward pools per era can be read from the `Inflation` pallet, by reading the `ActiveInflationConfig` storage value.
192+
193+
Each tier gets a portion of the reward pool (denoted as `reward_portion` in the configuration). These portions are further partitioned per slots.
194+
195+
E.g. for tier 1 dApp reward is calculated as:
196+
197+
`tier_1_dapp_reward = dapp_reward_pool_per_era * reward_portion[0] / slots_per_tier[0]`
198+
199+
### When To Call Expired Entry Cleanup
200+
201+
Each account can have a limited amount of `ContractStakeEntries`. This is denoted by `MaxNumberOfStakedContracts` runtime constant.
202+
203+
If an account has number of contract stake entries equal to the limit, calling `stake` might fail due to an `TooManyStakedContracts` error.
204+
A special extrinsic call, `cleanup_expired_entries` can be used to do the cleanup of expired entries to help with this problem.
205+
206+
Entry is considered to be expired if:
207+
1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward.
208+
2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not.
209+
210+
However, it is possible that the aforementioned cleanup call won’t work if the staker account is trying to stake on more contracts than it is allowed.
211+
In that case, staker should simply claim their pending rewards before attempting future actions.
212+
213+
### Runtime API
214+
215+
Runtimes supporting `dapp-staking-v3` functionality will also expose runtime API called: `DappStakingApi`.
216+
217+
Please refer [here](https://github.com/AstarNetwork/Astar/tree/master/pallets/dapp-staking-v3/rpc/runtime-api) for a list of supported functions.

docs/build/dapp-staking.md docs/build/dapp-staking/index.md

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ sidebar_position: 5
55
# dApp Staking
66

77
All content relating to dApp Staking has been moved to other sections of the documentation:
8+
89
- For general and technical documentation on dApp Staking, refer to [dApp Staking](/docs/learn/dapp-staking/) in the Learn section ;
910
- For Stakers, refer to [dApp Staking for stakers](/docs/use/dapp-staking/for-stakers/) in the Use section;
1011
- For projects looking to join the dApp Staking program, refer to [dApp Staking for devs](/docs/use/dapp-staking/for-devs/) in the Use section;
@@ -16,3 +17,10 @@ If you are interested in developing on top of dApp Staking and integrating dApp
1617
Learn how to integrate dApp staking into your EVM dApp in the precompiles chapter:
1718

1819
[EVM Precompiled Contracts](/docs/build/evm/precompiles/staking/)
20+
21+
### Other page may be of interest:
22+
23+
import DocCardList from '@theme/DocCardList';
24+
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
25+
26+
<DocCardList items={useCurrentSidebarCategory().items}/>

0 commit comments

Comments
 (0)