Howling Amethyst Moth
Medium
The earning power of the user is always updated to ensure that accurate reward distribution is applied at every given opportunity but this is skipped in the Claimreward function when payout is Zero.
Not updating earning power just because the payout is presently Zero will impact the reward earned by the user. This also breaks the intended flow as done in all other functions
/// @notice Internal convenience method which claims earned rewards.
/// @return Amount of reward tokens claimed, after the claim fee has been assessed.
/// @dev This method must only be called after proper authorization has been completed.
/// @dev See public claimReward methods for additional documentation.
function _claimReward(DepositIdentifier _depositId, Deposit storage deposit, address _claimer)
internal
virtual
returns (uint256)
{
_checkpointGlobalReward();
_checkpointReward(deposit);
uint256 _reward = deposit.scaledUnclaimedRewardCheckpoint / SCALE_FACTOR;
// Intentionally reverts due to overflow if unclaimed rewards are less than fee.
uint256 _payout = _reward - claimFeeParameters.feeAmount;
@audit >> early exit>> if (_payout == 0) return 0;
// retain sub-wei dust that would be left due to the precision loss
deposit.scaledUnclaimedRewardCheckpoint =
deposit.scaledUnclaimedRewardCheckpoint - (_reward * SCALE_FACTOR);
emit RewardClaimed(_depositId, _claimer, _payout);
@audit >> uint256 _newEarningPower =
earningPowerCalculator.getEarningPower(deposit.balance, deposit.owner, deposit.delegatee);
@audit >> early exit>> totalEarningPower =
_calculateTotalEarningPower(deposit.earningPower, _newEarningPower, totalEarningPower);
@audit >> early exit>> depositorTotalEarningPower[deposit.owner] = _calculateTotalEarningPower(
deposit.earningPower, _newEarningPower, depositorTotalEarningPower[deposit.owner]
);
@audit >> early exit>> deposit.earningPower = _newEarningPower.toUint96();
SafeERC20.safeTransfer(REWARD_TOKEN, _claimer, _payout);
if (claimFeeParameters.feeAmount > 0) {
SafeERC20.safeTransfer(
REWARD_TOKEN, claimFeeParameters.feeCollector, claimFeeParameters.feeAmount
);
}
return _payout;
}
When there is a change in the earning power calculation and a user calls to claim not updating the new earning power means the user gets to use an inflated/deflated earning power.
function rewardPerTokenAccumulated() public view virtual returns (uint256) {
if (totalEarningPower == 0) return rewardPerTokenAccumulatedCheckpoint;
@audit>> return rewardPerTokenAccumulatedCheckpoint
+ (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalEarningPower;
}
If the delegate is no longer eligible for example earning power remains the same instead of resetting to zero.
function getEarningPower(uint256 _amountStaked, address, /* _staker */ address _delegatee)
external
view
returns (uint256)
{
if (_isOracleStale() || isOraclePaused) return _amountStaked;
@audit>> return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
}
NOTE: If we are still within delay bump function will not work, hence updating the earning power AT ANY GIVEN opportunity is very important.
if (!_isDelegateeEligible(_delegatee)) {
bool _isUpdateDelayElapsed =
(timeOfIneligibility[_delegatee] + updateEligibilityDelay) <= block.timestamp;
@audit>> return (0, _isUpdateDelayElapsed);
}
https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L706-L745
Manual Review
Update the earning power as it is done in all other functions regardless of the payout amount.