52104 sc high removed reward tokens block validator commission claims

Submitted on Aug 8th 2025 at 00:57:27 UTC by @jovi for Attackathon | Plume Network

  • Report ID: #52104

  • 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

  • Theft of unclaimed yield

Description

Both ValidatorFacet.requestCommissionClaim() and ValidatorFacet.finalizeCommissionClaim() require that the commission token is still active via the _validateIsToken modifier. When governance calls RewardsFacet.removeRewardToken(), the token’s isRewardToken flag is cleared while all previously-accrued commission remains recorded. Every future claim therefore reverts, freezing the funds and stripping validators of their earnings.

Details

How commissions are supposed to work

1

Each validator accrues commission

Each validator earns a slice of every active reward token; the amount is tracked to be claimed as commission.

2

Claim flow

When the validator admin is ready, they call requestCommissionClaim() and later finalizeCommissionClaim() to receive the payout.

Both claim functions enforce the same modifier:

modifier _validateIsToken(address token) {     
    if (!PlumeStakingStorage.layout().isRewardToken[token]) {         
        revert TokenDoesNotExist(token);     
    }     
    _; 
}

As long as isRewardToken[token] is true, claims succeed. If the flag is ever false, the call reverts before any business logic executes.

What happens during token removal

Governance (or the Reward Manager) can retire a reward token:

function removeRewardToken(address token) external … {

    $.isRewardToken[token] = false;          // ① mark as inactive
    $.rewardRates[token]   = 0;              // stop further accrual

}

Once step ① runs:

  • The next call to requestCommissionClaim(..., token) or finalizeCommissionClaim(..., token) hits _validateIsToken and reverts with TokenDoesNotExist.

  • The commission becomes unclaimable.

Impact

Proof of Concept

1

Initial state

  • Deploy contracts.

  • Add reward token TOKEN_A; validators accrue commission in TOKEN_A.

  • Validator V has validatorAccruedCommission[V][TOKEN_A] = 1_000.

2

Governance action

  • Call RewardsFacet.removeRewardToken(TOKEN_A).

3

Attempted claim

  • V calls ValidatorFacet.requestCommissionClaim(V_ID, TOKEN_A) → reverts TokenDoesNotExist.

4

Observation

  • Storage still shows validatorAccruedCommission[V][TOKEN_A] == 1_000, but there is no callable function that will ever succeed, so the funds are frozen.

Was this helpful?