Skinny Mocha Dragon
High
The BinaryEligibilityOracleEarningPowerCalculator
contract allows admin functions to change eligibility criteria without checkpointing accumulated rewards, leading to permanent loss of unclaimed rewards for users when their earning power suddenly drops to zero,specialty when the processes happen in edge cases make the bumpers can't react .
The contract has multiple admin functions that can instantly change a delegatee's eligibility status:
setDelegateeScoreEligibilityThreshold()
setDelegateeScoreLock()
setOracleState()
When any of these functions are called and cause a delegatee to become ineligible, the earning power of all their stakers immediately drops to zero:
function getEarningPower(...) external view returns (uint256) {
if (_isOracleStale() || isOraclePaused) return _amountStaked;
return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
}
However, these functions don't trigger a checkpoint of accumulated rewards. The rewards calculation in GovernanceStaker depends on earning power:
function rewardPerTokenAccumulated() public view returns (uint256) {
if (totalEarningPower == 0) return rewardPerTokenAccumulatedCheckpoint;
return rewardPerTokenAccumulatedCheckpoint
+ (scaledRewardRate * (lastTimeRewardDistributed() - lastCheckpointTime)) / totalEarningPower;
}
- Users stake with a delegatee (earning power > 0)
- Rewards accumulate over time
- Admin increases eligibility threshold or triggers other eligibility changes
- Delegatee becomes ineligible
- All stakers' earning power drops to 0
- Unclaimed rewards between last checkpoint and eligibility change are permanently lost
Users can permanently lose their unclaimed rewards when their delegatee becomes ineligible through admin actions. This affects:
- All stakers of a delegatee simultaneously
- Any unclaimed rewards since last checkpoint
- No warning or time to claim rewards
- Multiple admin functions can trigger this
// BinaryEligibilityOracleEarningPowerCalculator.sol
function setDelegateeScoreEligibilityThreshold(uint256 _newDelegateeScoreEligibilityThreshold) public {
_checkOwner();
_setDelegateeScoreEligibilityThreshold(_newDelegateeScoreEligibilityThreshold);
// No checkpoint triggered
}
function getEarningPower(uint256 _amountStaked, address, address _delegatee)
external
view
returns (uint256)
{
if (_isOracleStale() || isOraclePaused) return _amountStaked;
return _isDelegateeEligible(_delegatee) ? _amountStaked : 0;
}
Manual Review
Implement automatic checkpointing for all eligibility-affecting functions.