51946 sc high commission claims fail for removed reward tokens
Submitted on Aug 6th 2025 at 19:23:49 UTC by @aksoy for Attackathon | Plume Network
Report ID: #51946
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
Brief/Intro
Validators are unable to request their accrued commissions if a reward token is removed from the contract. Although stakers can still claim their pending rewards for removed tokens, validators are blocked by a missing logic path, leading to unrecoverable commissions.
Vulnerability Details
The requestCommissionClaim function enforces a _validateIsToken modifier that reverts if the provided token is not marked as an active reward token in isRewardToken. Once a token is removed via removeRewardToken, its isRewardToken[token] mapping is set to false, causing any future commission claims to revert with TokenDoesNotExist(token). Validators cannot request commission claims because of the strict token validation enforced.
modifier _validateIsToken(
address token
) {
if (!PlumeStakingStorage.layout().isRewardToken[token]) {
revert TokenDoesNotExist(token);
}
_;
}
function requestCommissionClaim(
uint16 validatorId,
address token
)
external
onlyValidatorAdmin(validatorId)
nonReentrant
_validateValidatorExists(validatorId)
_validateIsToken(token)
{
...
}Impact Details
Validators may permanently lose access to their accrued commissions for any token that gets removed.
While stakers can still claim removed tokens’ rewards due to historical checkpointing, validators lack such an alternative path.
References
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/ValidatorFacet.sol#L508
Proof of Concept
Add test to file :: plume/test/PlumeStakingDiamond.t.sol
function testRemovedTokenRequestCommmission() public {
uint16 validatorId = DEFAULT_VALIDATOR_ID;
address token = address(pUSD);
// address recipient = validatorAdmin; // Not used by name
// Set up commission
vm.startPrank(validatorAdmin);
ValidatorFacet(address(diamondProxy)).setValidatorCommission(
validatorId,
10e16
); // 10%
vm.stopPrank();
// Set reward rate and fund treasury
vm.startPrank(admin);
address[] memory tokensToSet = new address[](1); // Renamed
tokensToSet[0] = token;
uint256[] memory ratesToSet = new uint256[](1); // Renamed
ratesToSet[0] = 1e18; // 1 PUSD per second
RewardsFacet(address(diamondProxy)).setRewardRates(
tokensToSet,
ratesToSet
);
// pUSD.transfer(address(treasury), 2000 ether); // Ensure enough funds
pUSD.transfer(address(treasury), 10e24); // Increased funding
vm.stopPrank();
// Stake to accrue commission
vm.startPrank(user1);
StakingFacet(address(diamondProxy)).stake{value: 10 ether}(validatorId);
vm.stopPrank();
// Advance time to accrue commission
vm.warp(block.timestamp + 1 days);
vm.startPrank(admin);
RewardsFacet(address(diamondProxy)).removeRewardToken(token);
vm.stopPrank();
// Request commission claim
vm.startPrank(validatorAdmin);
uint256 tsBeforeRequest = block.timestamp; // Capture timestamp BEFORE request
vm.expectRevert(
abi.encodeWithSelector(TokenDoesNotExist.selector, token)
);
ValidatorFacet(address(diamondProxy)).requestCommissionClaim(
validatorId,
token
);
}Was this helpful?