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 _validateIsToken will 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?