ZivoeRewards as well as ZivoeRewardsVesting use a function depositReward to deposit a reward to be vested over time. Claimable rewards are emitted every second according to rewardRate. When depositReward is called and a reward period is still ongoing, the newly deposited reward is added with remaining period reward and a new period is created, so a new rewardRate is calculated. depositReward however, can be called by anyone and it does not check if a 0 amount is passed as the reward amount. If someone calls does that, then the current rewardRate is diluted based on how far into the period in time the current block is.
function depositReward(address _rewardsToken, uint256 reward) external updateReward(address(0)) nonReentrant { //qq no 0 check reward
IERC20(_rewardsToken).safeTransferFrom(_msgSender(), address(this), reward);
// Update vesting accounting for reward (if existing rewards being distributed, increase proportionally).
if (block.timestamp >= rewardData[_rewardsToken].periodFinish) { //= vesting over at this point in time
rewardData[_rewardsToken].rewardRate = reward.div(rewardData[_rewardsToken].rewardsDuration); //= rewards per second
} else { //=before old vesting finished
uint256 remaining = rewardData[_rewardsToken].periodFinish.sub(block.timestamp);//= remaining seconds
uint256 leftover = remaining.mul(rewardData[_rewardsToken].rewardRate); //= remaining seconds * current reward rate
rewardData[_rewardsToken].rewardRate = reward.add(leftover).div(rewardData[_rewardsToken].rewardsDuration); // rewards rate = (total reward) / dura
}
rewardData[_rewardsToken].lastUpdateTime = block.timestamp;
rewardData[_rewardsToken].periodFinish = block.timestamp.add(rewardData[_rewardsToken].rewardsDuration);
emit RewardDeposited(_rewardsToken, reward, _msgSender());
}
The final line uses rewardsDuration, which is the duration of a full period. So essentially a new period is started without adding rewards in this case.
Impact
Rewards emitted per second will be lower for stakers.
heedfxn
medium
Reward rate can be diluted in rewards contracts
heedfxn
medium
Summary
Reward rate can be diluted in rewards contracts
Vulnerability Detail
ZivoeRewards
as well asZivoeRewardsVesting
use a functiondepositReward
to deposit a reward to be vested over time. Claimable rewards are emitted every second according torewardRate
. WhendepositReward
is called and a reward period is still ongoing, the newly deposited reward is added with remaining period reward and a new period is created, so a newrewardRate
is calculated.depositReward
however, can be called by anyone and it does not check if a 0 amount is passed as the reward amount. If someone calls does that, then the currentrewardRate
is diluted based on how far into the period in time the current block is.The final line uses
rewardsDuration
, which is the duration of a full period. So essentially a new period is started without adding rewards in this case.Impact
Rewards emitted per second will be lower for stakers.
Code Snippet
https://github.com/sherlock-audit/2024-03-zivoe/blob/d4111645b19a1ad3ccc899bea073b6f19be04ccd/zivoe-core-foundry/src/ZivoeRewards.sol#L228 https://github.com/sherlock-audit/2024-03-zivoe/blob/d4111645b19a1ad3ccc899bea073b6f19be04ccd/zivoe-core-foundry/src/ZivoeRewardsVesting.sol#L352
Tool used
Manual Review
Recommendation
Include a minimum amount for reward to be used in
depositReward
in both contracts, or add access modifiers to ensure it is not called with low values.Duplicate of #11