Rewards does not increase correctly due to rounding down
Description
The Rewards Manager is responsible for distributing rewards to stakers. It calculates the total reward amount to be distributed over time.
function _getNextDripAmount(uint256 totalBaseAmount_, IDripModel dripModel_, uint256 lastDripTime_)
internal
view
override
returns (uint256)
{
if (rewardsManagerState == RewardsManagerState.PAUSED) return 0;
uint256 dripFactor_ = dripModel_.dripFactor(lastDripTime_);
if (dripFactor_ > MathConstants.WAD) revert InvalidDripFactor();
return _computeNextDripAmount(totalBaseAmount_, dripFactor_);
}
function _previewNextRewardDrip(RewardPool storage rewardPool_) internal view returns (RewardDrip memory) {
return RewardDrip({
rewardAsset: rewardPool_.asset,
amount: _getNextDripAmount(rewardPool_.undrippedRewards, rewardPool_.dripModel, rewardPool_.lastDripTime)
});
}
function _dripRewardPool(RewardPool storage rewardPool_) internal override {
RewardDrip memory rewardDrip_ = _previewNextRewardDrip(rewardPool_);
if (rewardDrip_.amount > 0) {
rewardPool_.undrippedRewards -= rewardDrip_.amount;
rewardPool_.cumulativeDrippedRewards += rewardDrip_.amount;
}
rewardPool_.lastDripTime = uint128(block.timestamp);
}
When Rewards Manager calculates the amount of rewards for a user, it internally updates the index snapshot, which is the total amount of reward divided by the total supply of staking receipt tokens.
// Round down, in favor of leaving assets in the pool.
uint256 unclaimedDrippedRewards_ = cumulativeDrippedRewards_.mulDivDown(rewardsWeight_, MathConstants.ZOC)
- claimableRewardsData_.cumulativeClaimableRewards;
nextClaimableRewardsData_.cumulativeClaimableRewards += unclaimedDrippedRewards_;
// Round down, in favor of leaving assets in the claimable reward pool.
nextClaimableRewardsData_.indexSnapshot += unclaimedDrippedRewards_.divWadDown(stkReceiptTokenSupply_);
In order to calculate the share of reward for a user the index snapshot is multiplied by the amount of staking receipt tokens. Once the reward is processed for a user, the current index snapshot is stored to the user information for managing the amount of claimed reward.
function _previewUpdateUserRewardsData(
uint256 userStkReceiptTokenBalance_,
uint256 newIndexSnapshot_,
UserRewardsData storage userRewardsData_
) internal view returns (UserRewardsData memory newUserRewardsData_) {
newUserRewardsData_.accruedRewards = userRewardsData_.accruedRewards
+ _getUserAccruedRewards(userStkReceiptTokenBalance_, newIndexSnapshot_, userRewardsData_.indexSnapshot);
newUserRewardsData_.indexSnapshot = newIndexSnapshot_;
}
function _getUserAccruedRewards(
uint256 stkReceiptTokenAmount_,
uint256 newRewardPoolIndex,
uint256 oldRewardPoolIndex
) internal pure returns (uint256) {
// Round down, in favor of leaving assets in the rewards pool.
return stkReceiptTokenAmount_.mulWadDown(newRewardPoolIndex - oldRewardPoolIndex);
}
If the decimal of the staking receipt token, which is of the staked token, is greater than the decimal of the reward token, the total reward for a short period, such as seconds, can become zero when divided by the total supply of the staking receipt token.
Assume the TKN18 token, which has 18 decimals, is staked, and the TKN9 token, which has 9 decimals, is rewarded. Suppose 10,000 TKN18 is staked in the Rewards Manager, and the total reward for a year is 100 TKN9. The total reward for three seconds would be slightly less than 0.00001 TKN9. This reward amount is rounded down to zero when divided by the total supply of the staking receipt token. Therefore, if the index-snapshot updating logic is invoked three seconds after it was invoked last time, the index snapshot will not increase for that period of three seconds.
Exploiting this, an attacker can invoke the index-snapshot updating logic very frequently in order to prevent the index snapshot from increasing.
Impact
An attacker can prevent users from claiming rewards accrued during the attack.
Recommendations
Consider refactoring the reward logic or improving the accuracy of the index-snapshot calculation.
Remediation
This issue has been acknowledged by Cozy Finance Inc., and a fix was implemented in commit 2a2c49c0↗.