Rewards Manager Accounting

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).

Reward Pools and Drip

The RewardPool struct stores undripped rewards and cumulative dripped rewards. At this stage, there is no concept of stake pools or stakers.

struct RewardPool {
  // The amount of undripped rewards held by the reward pool.
  uint256 undrippedRewards;
  // The cumulative amount of rewards dripped since the last config update. This value is reset to 0 on each config
  // update.
  uint256 cumulativeDrippedRewards;
  // The last time undripped rewards were dripped from the reward pool.
  uint128 lastDripTime;
  ...
}

When do rewards drip?

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.redeemUndrippedRewards

  • RewardsManager.dripRewardPool

How exactly does drip work?

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:

dripFactor = dripModel.dripFactor(lastDripTime);
drippedRewards = dripFactor * rewardPool.undrippedRewards;

Claimable Rewards

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.

struct ClaimableRewardsData {
  // The cumulative amount of rewards that are claimable on behalf of all users. This value is reset to 0 on each
  // config update.
  uint256 cumulativeClaimableRewards;
  // The index snapshot the relevant claimable rewards data, when the cumulative claimed rewards were updated. The index
  // snapshot must update each time the cumulative claimed rewards are updated.
  uint256 indexSnapshot;
}

When do claimable rewards get updated?

The following operations trigger an update to claimableRewards:

  • RewardsManager.claimRewards -> _claimRewards

  • RewardsManager.stake and RewardsManager.stakeWithoutTransfer -> _dripAndApplyPendingDrippedRewards

  • RewardsManager.unstake -> _claimRewards

  • RewardsManger.updateConfigs -> _dripAndResetCumulativeRewardsValues

How exactly do claimable rewards work?

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:

claimableRewards[stakePoolId][rewardPoolId].cumulativeClaimableRewards <=
    rewardPools[rewardPoolId].cumulativeDrippedRewards.mulDivDown(stakePools[stakePoolId].rewardsWeight, ZOC)

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:

indexSnapshot += x.divWadDown(stakePools[stakePoolId].stkReceiptToken.totalSupply())

User Rewards

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.

struct UserRewardsData {
  // The total amount of rewards accrued by the user.
  uint256 accruedRewards;
  // The index snapshot the relevant claimable rewards data, when the user's accrued rewards were updated. The index
  // snapshot must update each time the user's accrued rewards are updated.
  uint256 indexSnapshot;
}

When do user rewards get updated?

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

How exactly do user rewards work?

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:

oldRewardPoolIndex = userRewards[stakePoolId][staker][rewardPoolId].indexSnapshot
newRewardPoolIndex = claimableRewards[stakePoolId][rewardPoolId].indexSnapshot
accruedRewards += stakerReceiptTokenBalance.mulWadDown(newRewardPoolIndex - oldRewardPoolIndex);

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.

Config Updates and Claimable Rewards

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.

StkReceiptToken Transfers

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, stkReceiptTokens 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.

Last updated