|
| 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. |
0 commit comments