Skip to content

Latest commit

 

History

History
90 lines (71 loc) · 3.83 KB

023.md

File metadata and controls

90 lines (71 loc) · 3.83 KB

Proud Tartan Lynx

Medium

Get Reward Without earningPower

Summary

Even if someone's new earningPower is zero and they have no rewards, they can still receive rewards amounting to maxBumpTip - claimFeeParameters.feeAmount.

Root Cause

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

Internal pre-conditions

  1. maxBumpTip > claimFeeParameters.feeAmount.

External pre-conditions

N/A

Attack Path

  1. A malicious user has n stakes, each with an amount of 1.
  2. Wait untill all new earningPower is zero.
  3. The user's unclaimedRewards will reach n * maxBumpTip soon after the earningPower of others updates to zero.
  4. The user can claim rewards totalingn * (maxBumpTip - claimFeeParameters.feeAmount)

Impact

Users can obtain rewards without earningPower. If the new totalEarningPower is zero, even if their deposit amount is 1, they can soon receive rewards amounting to maxBumpTip - claimFeeParameters.feeAmount. If there are many stakes with an amount of 1, the protocol must send the rewards amounting to maxBumpTip - claimFeeParameters.feeAmount to each one. This results in a loss for the protocols.

PoC

GovernanceStaker.sol
471:    function bumpEarningPower(
            DepositIdentifier _depositId,
            address _tipReceiver,
            uint256 _requestedTip
        ) external virtual {
            if (_requestedTip > maxBumpTip) revert GovernanceStaker__InvalidTip();

            Deposit storage deposit = deposits[_depositId];

            _checkpointGlobalReward();
            _checkpointReward(deposit);

            uint256 _unclaimedRewards = deposit.scaledUnclaimedRewardCheckpoint / SCALE_FACTOR;

            (uint256 _newEarningPower, bool _isQualifiedForBump) = earningPowerCalculator.getNewEarningPower(
                deposit.balance, deposit.owner, deposit.delegatee, deposit.earningPower
            );
            if (!_isQualifiedForBump || _newEarningPower == deposit.earningPower) {
                revert GovernanceStaker__Unqualified(_newEarningPower);
            }

            if (_newEarningPower > deposit.earningPower && _unclaimedRewards < _requestedTip) {
                revert GovernanceStaker__InsufficientUnclaimedRewards();
            }

            // Note: underflow causes a revert if the requested  tip is more than unclaimed rewards
497:        if (_newEarningPower < deposit.earningPower && (_unclaimedRewards - _requestedTip) < maxBumpTip)
            {
                revert GovernanceStaker__InsufficientUnclaimedRewards();
            }

            // Update global earning power & deposit earning power based on this bump
            totalEarningPower =
            _calculateTotalEarningPower(deposit.earningPower, _newEarningPower, totalEarningPower);
            depositorTotalEarningPower[deposit.owner] = _calculateTotalEarningPower(
                deposit.earningPower, _newEarningPower, depositorTotalEarningPower[deposit.owner]
            );
            deposit.earningPower = _newEarningPower.toUint96();

            // Send tip to the receiver
            SafeERC20.safeTransfer(REWARD_TOKEN, _tipReceiver, _requestedTip);
            deposit.scaledUnclaimedRewardCheckpoint =
            deposit.scaledUnclaimedRewardCheckpoint - (_requestedTip * SCALE_FACTOR);
        }

When the user's new earningPower is zero and _unclaimedRewards is less than maxBumpTip, the bumpEarningPower function always reverts. Thus, the user's _unclaimedRewards can reach maxBumpTip, allowing them to claim maxBumpTip - claimFeeParameters.feeAmount

Mitigation

794:    function _setMaxBumpTip(uint256 _newMaxTip) internal virtual {
            emit MaxBumpTipSet(maxBumpTip, _newMaxTip);
+           require(_newMaxTip <= claimFeeParameters.feeAmount,"");
            maxBumpTip = _newMaxTip;
        }