50082 sc low protocol lets validators operate with dust amounts making attacks risk free
Submitted on Jul 21st 2025 at 15:10:02 UTC by @holydevoti0n for Attackathon | Plume Network
Report ID: #50082
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol
Impacts:
Permanent freezing of funds
Protocol insolvency
Description
Brief/Intro
The _unstake function doesn't check if the remaining stake amount is below the minimum stake amount, allowing validators to operate actively while having a dust amount of funds staked (i.e., 1 wei).
Vulnerability Details
The protocol does not enforce the minimum stake amount when a user partially unstakes. It allows stakers/validators to remain with stake amounts below minStakeAmount.
Vulnerable snippet:
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/StakingFacet.sol#L366-L390 function _unstake(uint16 validatorId, uint256 amount) internal returns (uint256 amountToUnstake) {
PlumeStakingStorage.Layout storage $s = PlumeStakingStorage.layout();
// @audit - Validations does not validate min stake amount
_validateValidatorForUnstaking(validatorId);
if (amount == 0) {
revert InvalidAmount(amount);
}
if ($s.userValidatorStakes[msg.sender][validatorId].staked < amount) {
revert InsufficientFunds($s.userValidatorStakes[msg.sender][validatorId].staked, amount);
}
...
return amount;
}With the current logic, validators can operate with dust amounts, increasing the likelihood of validators acting maliciously. The invariant that every active validator holds at least the minimum stake amount is broken.
Examples of consequences:
Two malicious validators could skip voting in the slashing process to avoid each other being slashed.
Admin deactivates the two validators above; new/existing validators can repeat the same behavior when their stake/funds are dust/below the minimum stake amount.
Malicious validators acting together can vote to slash legitimate validators, reducing their funds to zero.
Impact
The protocol permits partial unstaking that drops validators below minimum stake requirements, enabling validators to act maliciously with minimal financial risk.
Recommendation
Enforce the minimum stake amount when a user partially unstakes. Suggested patch:
function _unstake(uint16 validatorId, uint256 amount) internal returns (uint256 amountToUnstake) {
PlumeStakingStorage.Layout storage $s = PlumeStakingStorage.layout();
...
+ if ($s.userValidatorStakes[msg.sender][validatorId].staked - amount < $s.minStakeAmount) {
+ revert StakeAmountTooSmall($s.userValidatorStakes[msg.sender][validatorId].staked - amount, $s.minStakeAmount);
+ }
...
}Proof of Concept
Add the PoC below into PlumeStakingDiamond.t.sol:
function testStakerCanLeaveValidatorWithDustAmountStaked() public {
uint256 amount = 1e18;
vm.startPrank(user1);
StakingFacet(address(diamondProxy)).stake{value: amount}(
DEFAULT_VALIDATOR_ID
);
assertEq(StakingFacet(address(diamondProxy)).amountStaked(), amount);
// Unstake
StakingFacet(address(diamondProxy)).unstake(DEFAULT_VALIDATOR_ID, 0.9e18);
assertEq(StakingFacet(address(diamondProxy)).amountCooling(), 0.9e18);
assertEq(StakingFacet(address(diamondProxy)).amountStaked(), 0.1e18);
// print total staked for this validator
(,uint256 validatorTotalStaked,) = ValidatorFacet(address(diamondProxy)).getValidatorInfo(DEFAULT_VALIDATOR_ID);
console2.log("minStakeAmount: %e", ManagementFacet(address(diamondProxy)).getMinStakeAmount());
console2.log("Validator has a total staked of %e", validatorTotalStaked);
vm.stopPrank();
}Run:
forge test --mt testStakerCanLeaveValidatorWithDustAmountStaked -vv --via-irOutput:
[PASS] testStakerCanLeaveValidatorWithDustAmountStaked() (gas: 554340)
Logs:
minStakeAmount: 1e18
Validator has a total staked of 1e17Was this helpful?