51999 sc high logical flaw in validator reactivation and addrewardtoken allows claiming rewards for validators in inactive periods
Description
Short summary
The vulnerability
1
RewardsFacet.addRewardToken creates checkpoints for inactive validators
uint16[] memory validatorIds = $.validatorIds;
for (uint256 i = 0; i < validatorIds.length; i++) {
uint16 validatorId = validatorIds[i];
// @audit-issue No check for validator status. Creates checkpoint even for inactive validators.
PlumeRewardLogic.createRewardRateCheckpoint($, token, validatorId, initialRate);
}2
ValidatorFacet clears slashedAtTimestamp upon reactivation
// If going ACTIVE: reset timestamps and clear the timestamp cap
if (newActiveStatus && !currentStatus) {
// Create a new checkpoint to restore the reward rate, signaling activity resumes
for (uint256 i = 0; i < rewardTokens.length; i++) {
address token = rewardTokens[i];
$.validatorLastUpdateTimes[validatorId][token] = block.timestamp;
uint256 currentGlobalRate = $.rewardRates[token];
PlumeRewardLogic.createRewardRateCheckpoint($, token, validatorId, currentGlobalRate);
}
// Clear the timestamp since validator is active again (unless actually slashed)
if (!validator.slashed) {
validator.slashedAtTimestamp = 0; // @audit-issue Erases the history of the inactive period.
}
}3
Reward calculation is memoryless with respect to erased downtime
updateRewardsForValidator ==> updateRewardsForValidatorAndToken ==> calculateRewardsWithCheckpoints ==> _calculateRewardsCorefunction updateRewardsForValidatorAndToken(
PlumeStakingStorage.Layout storage $,
address user,
uint16 validatorId,
address token
) internal {
(uint256 userRewardDelta,,) =
calculateRewardsWithCheckpoints($, user, validatorId, token, userStakedAmount); // @audit calculateRewardsWithCheckpoints will call _calculateRewardsCore to calculate rewards
if (userRewardDelta > 0) {
$.userRewards[user][validatorId][token] += userRewardDelta; // @audit userRewardDelta is added for user to claim
$.totalClaimableByToken[token] += userRewardDelta;
$.userHasPendingRewards[user][validatorId] = true;
}
}function _calculateRewardsCore(
PlumeStakingStorage.Layout storage $,
address user,
uint16 validatorId,
address token,
uint256 userStakedAmount,
uint256 currentCumulativeRewardPerToken
)
{
// Then check validator slash/inactive timestamp
if (validator.slashedAtTimestamp > 0) { // @audit In case reactivation, the slashedAtTimestamp = 0 so there is no memory about inactive period.
if (validator.slashedAtTimestamp < effectiveEndTime) {
effectiveEndTime = validator.slashedAtTimestamp;
}
}
// If no time has passed or user hasn't earned anything yet (e.g. paid index is already current)
if (
effectiveEndTime <= lastUserRewardUpdateTime
|| currentCumulativeRewardPerToken <= lastUserPaidCumulativeRewardPerToken
) {
return (0, 0, 0);
}
effectiveTimeDelta = effectiveEndTime - lastUserRewardUpdateTime; // This is the total duration of interest
uint256[] memory distinctTimestamps =
getDistinctTimestamps($, validatorId, token, lastUserRewardUpdateTime, effectiveEndTime); // @audit the system will get all the checkpoints from lastUserRewardUpdateTime to effectiveEndTime including the inactive period . In this case the lastUserRewardUpdateTime is before the time the validator become inactive.
}Severity assessment
Suggested Fix / Remediation
Proof of Concept
Sequence Diagram
Previous52865 sc high inconsistency in how stake cooldown is handled due to off by one error Next52982 sc medium non standard erc20 approvals usdt like cause repeat call failures after partial fills
Was this helpful?