51124 sc high validator would loss commission fee if the rewards token are removed
Submitted on Jul 31st 2025 at 10:45:48 UTC by @pks271 for Attackathon | Plume Network
Report ID: #51124
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/ValidatorFacet.sol
Impacts:
Temporary freezing of funds for at least 24 hours
Description
Bug Description
When stake/unstake/claim including RewardsFacet#removeRewardToken actions are called, the $.validatorAccruedCommission[validatorId][token] += commissionDeltaForValidator inside PlumeRewardLogic#updateRewardPerTokenForValidator() function will be updated. This parameter tracks the history of validators' commission fees and validators can call ValidatorFacet#requestCommissionClaim/finalizeCommissionClaim to claim their commission fee.
When REWARD_MANAGER_ROLE calls RewardsFacet#removeRewardToken to remove the reward token, the $.validatorAccruedCommission[validatorId][token] is updated first, then $.isRewardToken[token] is set to false. The problem is that requestCommissionClaim will revert because it uses _validateIsToken to check whether the reward token exists; if not, it reverts.
Relevant code excerpts:
function removeRewardToken(
address token
) external onlyRole(PlumeRoles.REWARD_MANAGER_ROLE) {
PlumeStakingStorage.Layout storage $ = PlumeStakingStorage.layout();
if (!$.isRewardToken[token]) {
revert TokenDoesNotExist(token);
}
// Find the index of the token in the array
uint256 tokenIndex = _getTokenIndex(token);
// Store removal timestamp to prevent future accrual
uint256 removalTimestamp = block.timestamp;
$.tokenRemovalTimestamps[token] = removalTimestamp;
// Update validators (bounded by number of validators, not users)
// @audit - update the reward per token for all validators
for (uint256 i = 0; i < $.validatorIds.length; i++) {
uint16 validatorId = $.validatorIds[i];
// Final update to current time to settle all rewards up to this point
PlumeRewardLogic.updateRewardPerTokenForValidator($, token, validatorId);
// Create a final checkpoint with a rate of 0 to stop further accrual definitively.
PlumeRewardLogic.createRewardRateCheckpoint($, token, validatorId, 0);
}
// Set rate to 0 to prevent future accrual. This is now redundant but harmless.
$.rewardRates[token] = 0;
// DO NOT delete global checkpoints. Historical data is needed for claims.
// delete $.rewardRateCheckpoints[token];
// Update the array
$.rewardTokens[tokenIndex] = $.rewardTokens[$.rewardTokens.length - 1];
$.rewardTokens.pop();
// Update the mapping
// @audit - set `$.isRewardToken[token]` to false
$.isRewardToken[token] = false;
delete $.maxRewardRates[token];
emit RewardTokenRemoved(token);
}The _validateIsToken modifier currently reverts when isRewardToken[token] is false:
requestCommissionClaim uses this modifier, which causes it to revert for tokens that were valid reward tokens but were later removed after commission accrual:
Impact
Validators who accrued commission for a token that is later removed will be unable to request/finalize claims for that accrued commission because
_validateIsTokenwill revert if the token has been removed.This can temporarily freeze validator funds (commission) for at least 24 hours (or until the token is re-added or the contract is changed).
Recommendation
Consider adding a historical commission check to the token validation so that tokens that are no longer active but have accrued commission can still be claimed. For example:
This approach allows validators to claim previously accrued commission for tokens that have been removed while still preventing operations for tokens that were never registered.
Proof of Concept
Insert the following test case into PlumeStakingDiamond.t.sol:
Was this helpful?