52719 sc medium inactive validators blocked from claiming commissions despite passed timelock

  • Report ID: #52719

  • Report Type: Smart Contract

  • Report severity: Medium

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

  • Impacts:

    • Theft of unclaimed yield

Brief / Intro

When a commission claim for a validator and token is requested, a timelock is started, after which the request can be finalized. However, both requesting and finalizing are disallowed for inactive validators, even if the timelock had already passed before the validator became inactive. This restriction is stricter than for slashed validators, which is inconsistent and does not make sense.

Vulnerability Details

Validators can be either deactivated or slashed. Both events set the slashedAtTimestamp to the current block.timestamp. However, slashing a validator is far more severe: it means that all other validators voted to slash it due to malicious behavior. By contrast, deactivation simply means the validator is no longer active, without implying any wrongdoing.

Therefore, it is illogical to completely block commission claims for deactivated validators while still allowing certain commission claims for slashed validators.

Relevant code excerpts:

When a validator is slashed, the commission claim can still be finalized if the timelock had already passed before the validator was slashed:

        if (validator.slashed && readyTimestamp >= validator.slashedAtTimestamp) {
            revert ValidatorInactive(validatorId);
        }

In contrast, deactivated validators cannot finalize their requests at all, regardless of whether the timelock had already passed:

        // For a non-slashed validator, simply require it to be active to finalize a claim.
        if (!validator.slashed && !validator.active) {
            revert ValidatorInactive(validatorId);
        }

This creates an inconsistent and stricter rule for deactivated (inactive) validators compared to slashed validators.

Impact Details

References

Code references are provided throughout the report (links retained as-is):

  • ValidatorFacet.sol (target): https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/ValidatorFacet.sol

  • PlumeRewardLogic.sol lines referenced in PoC: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/lib/PlumeRewardLogic.sol#L189-L191

  • Request commission claim: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L500-L539

  • Request timestamp set: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L531

  • Requesting blocked for slashed or inactive: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L513-L515

  • setValidatorStatus (deactivation): https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L252-L308

  • Deactivation sets slashedAtTimestamp: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L277

  • finalizeCommissionClaim checks: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L566-L575

Proof of Concept

1

Step

Validator A accrues commissions.

Reference: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/lib/PlumeRewardLogic.sol#L189-L191

2

Step

The admin of validator A requests a commission claim by calling ValidatorFacet::requestCommissionClaim().

Reference: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L500-L539

This sets the requestTimestamp to the current block.timestamp: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L531

Note: Requesting a claim is not possible for slashed or inactive validators: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L513-L515

3

Step

After the timelock has passed, the validator is deactivated by calling ValidatorFacet::setValidatorStatus().

Reference: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L252-L308

Deactivation sets the slashedAtTimestamp to the current block.timestamp: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L277

4

Step

When the validator admin tries to finalize the commission claim by calling ValidatorFacet::finalizeCommissionClaim(), the call reverts even though the timelock had already passed while the validator was still active.

Reference (finalize checks): https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L566-L575

If the validator had been slashed instead, the call would not revert: https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L566-L570

This demonstrates the inconsistency: deactivated validators cannot finalize even if the timelock expired before deactivation, while slashed validators may finalize depending on timestamps.

Was this helpful?