Skip to content

Latest commit

 

History

History
111 lines (79 loc) · 3.91 KB

005.md

File metadata and controls

111 lines (79 loc) · 3.91 KB

Howling Amethyst Moth

Medium

When payout=0, the code fails to update the earning power of the user.

Summary

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.

Vulnerability Detail

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;
  }

Impact

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);
    }

Code Snippet

https://github.com/sherlock-audit/2024-11-tally/blob/main/staker/src/GovernanceStaker.sol#L706-L745

Tool used

Manual Review

Recommendation

Update the earning power as it is done in all other functions regardless of the payout amount.