50519 sc high rewardsfacet reintroducing an old reward token will result in wrong accounting leading to theft of yield

Submitted on Jul 25th 2025 at 16:40:46 UTC by @max10afternoon for Attackathon | Plume Network

  • Report ID: #50519

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/RewardsFacet.sol

  • Impacts:

    • Theft of unclaimed yield

Description

Brief/Intro

Reintroducing an old reward token will result in wrong accounting, leading to theft of yield.

NB. Notes on access control and limitations will be addressed in the "Access control & limitations" section of the report

Vulnerability Details

When staking on an empty account, only the userValidatorRewardPerTokenPaid, userValidatorRewardPerTokenPaidTimestamp and validatorRewardPerTokenCumulative variable of currently available reward tokens will be upgraded, and not the historical ones, as can be seen by the two functions involved _initializeRewardStateForNewStake and updateRewardPerTokenForValidator

    function _initializeRewardStateForNewStake(address user, uint16 validatorId) internal {
        PlumeStakingStorage.Layout storage $ = PlumeStakingStorage.layout();

        $.userValidatorStakeStartTime[user][validatorId] = block.timestamp;

        address[] memory rewardTokens = $.rewardTokens;
        for (uint256 i = 0; i < rewardTokens.length; i++) {
            address token = rewardTokens[i];
            if ($.isRewardToken[token]) {
                PlumeRewardLogic.updateRewardPerTokenForValidator($, token, validatorId);

                $.userValidatorRewardPerTokenPaid[user][validatorId][token] =
                    $.validatorRewardPerTokenCumulative[validatorId][token];
                $.userValidatorRewardPerTokenPaidTimestamp[user][validatorId][token] = block.timestamp;
            }
        }
    }

The validatorRewardPerTokenCumulative variable in particular will be update in updateRewardPerTokenForValidator, whenever updating the rewards for a particular token validator couple.

This two things combined will result in an incorrect reward distribution: in fact, if a user stakes after the reward token has been removed, their userValidatorRewardPerTokenPaidTimestamp variable will not be updated, while the token validator couple will still hold the old validatorRewardPerTokenCumulative variable. This means that if they claim after the reward tokens gets reintroduced, the user will receive all the rewards belonging to the check points, between their last complete unstake and the token removal, accruing rewards that happened after a full unstake, resulting in a theft of yield (this can be done maliciously or by accident).

Access control & limitations

In order for this to be possible the user must have staked some amount of tokens in the past with the validator, it is NOT necessary for the user to hold those stake assets at the time of the attack or to leave them in the contract for more than one block, as the stake action must be performed on an empty stakeInfo, it's just necessary for the userValidatorRewardPerTokenPaidTimestamp variable to be different than 0 to avoid a safety check in _calculateRewardsCore.

Although adding and removing reward tokens is an administrative action, the issue will NOT arise form a reckless or malicious behavior by the admins, but will also happen under a regular and safe use of the contract and administrative privileges. This is highlighted by the fact that the the addRewardToken functions has safety checks dedicated to the possibility of reintroducing a reward token

Also this report concerns the action that a PERMISSIONLESS user can do around a privileged action maliciously (by frontrunning it) or by accident.

The issue will arise only if there are existing legacy check points. Although it exist a functionality to remove such checkpoints, this will not always be usable as it can lead to user's rewards being permanently frozen in the contract or until an update, leading to an issue of equivalent severity (Temporary freezing of funds for at least 24 hours), as any rewards that haven't been computed yet by users, belonging to such checkpoints will be lost (as highlighted in multiple comments of similar functions, and the function it self).

Impact Details

Theft of unclaimed yield: A user will be able to get access to the yield generated from all the legacy checkpoints, that were active while they had 0 assets staked in the contract, resulting in a theft of yield.

Proof of Concept

As highlighted in the 'Access control & limitations' section this report is focused on the interaction that a permissionless user can have with a safe and reasonable use of administrative functions. Therefor the coded PoC will make use of such functions, that does not mean that privileged access is required to exploit the issue.

To run the script copy it inside the /attackathon-plume-network/plume/test folder

Was this helpful?