Lively Arctic Robin
Medium
Lack of Delegatee Eligibility Check May Result in Successful Staking but Immediately No Rewards due to no Update in Earning Power
The current implementation of the _stake
and _stakeMore
cfunction in the smart contract does not verify whether the delegatee is eligible for staking before proceeding with the staking operation. This oversight can lead to users being unaware that their stake has been processed without earning any rewards if the delegatee is ineligible. Additionally, the bumpEarningPower
function, which is used to update the earning power of deposits, checks delegatee eligibility and prevents updates for unqualified delegatees. This creates a mismatch between the two functions—staking is allowed without validation, but earning power cannot be updated if the delegatee is unqualified. The report recommends separating the getEarningPower
(used for querying) and getNewEarningPower
(used for staking and earning power updates) to improve clarity and functionality.
The existing code has two main issues:
-
Staking without Delegatee Eligibility Check:
- The
_stake
and_stakeMore
function does not validate whether the delegatee is eligible before proceeding with the staking operation. This can result in successful staking but without earning any rewards if the delegatee’s earning power is zero.
- The
-
Mismatch Between Staking and Earning Power Update Logic:
- The
bumpEarningPower
function checks if the delegatee is eligible using thegetNewEarningPower
function and requires the delegatee to be qualified before updating the earning power. However, the staking function does not perform this check. This discrepancy means that while staking can be performed with an unqualified delegatee, updates to the earning power viabumpEarningPower
will be blocked if the delegatee is unqualified.
- The
- Users may stake their tokens with an ineligible delegatee and later find that they are not earning rewards. This leads to confusion as the transaction appears successful, but no rewards are generated.
- There is a mismatch in the system’s behavior. While the
bumpEarningPower
function prevents updates for unqualified delegatees, staking does not. This inconsistency can create frustration for users who attempt to update their earning power after staking with an invalid delegatee.
Current implementation:
staker/src/GovernanceStaker.sol:_stake#L571-L73
function _stake(...) {
// ... other checks
// @audit Just calculates earning power without checking delegatee eligibility
uint256 _earningPower = earningPowerCalculator.getEarningPower(
_amount,
_depositor,
_delegatee
);
// @audit Directly proceed with staking operation
totalStaked += _amount;
// ... more logic
}
staker/src/GovernanceStaker.sol:_stakeMore#L605-L610
function _stakeMore(...) {
// @audit Just calculates earning power without checking delegatee eligibility
uint256 _newEarningPower = earningPowerCalculator.getEarningPower(
_newBalance,
deposit.owner,
deposit.delegatee
);
totalEarningPower = _calculateTotalEarningPower(
deposit.earningPower,
_newEarningPower,
totalEarningPower
);
// @audit Directly proceed with staking operation and earningPower without
totalStaked += _amount;
The following code snippet shows the current behavior of the bumpEarningPower
function, which checks delegatee eligibility before updating earning power:
staker/src/GovernanceStaker.sol:bumpEarningPower#L485-L490
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
);
// @audit check Earning power could not be updated due to unqualified delegatee
if (!_isQualifiedForBump || _newEarningPower == deposit.earningPower) {
revert GovernanceStaker__Unqualified(_newEarningPower);
}
// Further checks and updates for earning power...
}
The above code prevents updates if the delegatee is not qualified (_isQualifiedForBump
), which causes an issue when the delegatee is unqualified during staking, especially stakers just finish their staking .
Manual Review
To address these issues and enhance the functionality of the staking system, we recommend the following changes:
-
Delegatee Eligibility Check on Staking:
- Implement a delegatee eligibility check within the
_stake
function before proceeding with the staking operation. This will ensure that only qualified delegatees can receive stakes and avoid situations where the earning power is zero due to an unqualified delegatee.
- Implement a delegatee eligibility check within the
-
Clarify Usage of
getEarningPower
andgetNewEarningPower
:getEarningPower
should be used for querying the current earning power of a deposit, as it does not modify the state and provides a simple snapshot of the rewards a user can earn.getNewEarningPower
should be used for staking and earning power update operations. This function already checks delegatee eligibility and should be called only after ensuring the delegatee is eligible. This will prevent inconsistencies in how earning power is updated.