Rewards Manager Accounting
Last updated
Last updated
The graphic below provides a useful mental model of how rewards accounting works.
Conceptually, rewards flow from the RewardPool
struct, to ClaimableRewardsData
structs (one for each stake pool), to UserRewardsData
(one for each staker in a stake pool).
The RewardPool
struct stores undripped rewards and cumulative dripped rewards. At this stage, there is no concept of stake pools or stakers.
Rewards can either drip simultaneously for all reward pools or for a single reward pool.
Many operations in the Rewards Manager internally drip rewards. However, anyone can drip rewards on-demand by calling RewardsManager.dripRewards
and RewardsManager.dripRewardPool
, which are also public and external functions, respectively.
The following operations internally drip rewards for all reward pools:
RewardsManager.pause
RewardsManager.unpause
RewardsManager.claimRewards
RewardsManager.updateConfigs
RewardsManager.dripRewards
The following operations internally drip rewards for a single reward pool:
RewardsManager.stake
RewardsManager.stakeWithoutTransfer
RewardsManager.dripRewardPool
The core drip functionality is implemented in RewardsManager._dripRewardPool
. When a reward pool drips, the following happens:
undrippedRewards
gets decremented by the amount of dripped rewards.
cumulativeDrippedRewards
gets incremented by that same amount of dripped rewards.
lastDripTime
updates to block.timestamp
.
The amount of dripped rewards is simply:
The claimableRewards
storage variable is a nested mapping which maps stake pool IDs to reward pool IDs to a ClaimableRewardsData
struct.
Each ClaimableRewardsData
struct is used to track the claimable rewards associated with a given (stake pool, reward pool) pair.
The following operations trigger an update to claimableRewards
:
RewardsManager.claimRewards
-> _claimRewards
RewardsManager.stake
and RewardsManager.stakeWithoutTransfer
-> _dripAndApplyPendingDrippedRewards
RewardsManager.unstake
-> _claimRewards
RewardsManger.updateConfigs
-> _dripAndResetCumulativeRewardsValues
The claimableRewards
variable is lazily updated, which obviates the need to iterate through all (stake pool, reward pool) pairs when unnecessary.
The cumulativeClaimableRewards
value is the amount of RewardPool.cumulativeDrippedRewards
which are now fully "claimable" by stakers in a specific stake pool. Since it is lazily updated, it is a lagging value. Specifically, we have the following invariant always holds:
Each time cumulativeClaimableRewards
is updated, it is brought in sync with the right-hand side of the inequality above.
Each time cumulativeClaimableRewards
is updated, so is indexSnapshot
. The indexSnapshot
value represents the accrued rewards of a theoretical staker who owns a single stkReceiptToken
and began staking at initialization of the stake pool.
Say cumulativeClaimableRewards
is incremented by x
. Then:
The userRewards
storage variable is a nested mapping which maps stake pool IDs to staker addresses to an array of UserRewardsData
structs. Each UserRewardsData
struct is used to track the rewards a staker is entitled to for a given reward pool.
Any operation which updates a user's stkReceiptToken
balance or claims some of the user's accrued rewards triggers an update to userRewards
:
RewardsManager.claimRewards
-> _claimRewards
RewardsManager.stake
and RewardsManager.stakeWithoutTransfer
-> _updateUserRewards
RewardsManager.unstake
-> _claimRewards
StkReceiptToken.transfer
-> _updateUserRewardsForStkReceiptTokenTransfer
Much like claimable rewards, accruedRewards
is lazily updated since it is impossible to iterate through all stakers in a given stake pool.
Each time a user's stkReceiptToken
balance changes, accruedRewards
are updated as follows:
The special case is when the rewards are claimed. In that case, accuredRewards
is set to 0.
Whenever accruedRewards
is updated, the user rewards indexSnapshot
is also brought in sync with the claimable rewards indexSnapshot
.
Since new reward pools can be added after a user has staked, it is possible that userRewards[stakePoolId][staker].length <= rewardPools.length
. In that case, there is special handling to push a new UserRewardsData
struct.
Recall that the claimable rewards accounting crucially depends on an invariant, which uses the StakePool.rewardsWeight
.
It is possible that a Rewards Manager config update changes these weights and the invariant no longer holds. So, before any config update is applied, all claimable rewards data is fully reset. More specifically:
All reward pools are dripped.
ClaimableRewardsData.indexSnapshot
is fully updated for all (stake pool, reward pool) pairs.
RewardPool.cumulativeDrippedRewards
is reset to 0.
ClaimableRewardsData.cumulativeClaimedRewards
is reset to 0.
By resetting the cumulative rewards values to 0, we can use the invariant again to do accounting until there is another config update.
Stake receipt token transfers change user balances and so must get reflected in the Rewards Manager's user rewards accounting.
Prior to the standard IERC20.transfer
call, stkReceiptToken
s call RewardsManager.updateUserRewardsForStkReceiptTokenTransfer
. This function brings the UserRewardsData
for both the to
and from
address up to date, so rewards accrue properly after the transfer occurs.