The checks for whether _updatePeriodEffectiveStake should be called in the delegate and unstake functions are insufficient. This can cause an NFT's unstaking to potentially call _updatePeriodEffectiveStake twice and lead to failures in other NFTs' withdrawals due to insufficient delegatorsEffectiveStake, resulting in funds being locked.
Vulnerability Details
Relevant code (excerpt):
// if the delegation is pending or the validator is exited or unknown// decrease the effective stake of the previous validatorif( currentValidatorStatus == VALIDATOR_STATUS_EXITED || delegation.status == DelegationStatus.PENDING){// get the completed periods of the previous validator(,,,uint32 oldCompletedPeriods)= $.protocolStakerContract.getValidationPeriodDetails(delegation.validator);// decrease the effective stake of the previous validator_updatePeriodEffectiveStake( $, delegation.validator, _tokenId, oldCompletedPeriods +2,false// decrease);}
The _updatePeriodEffectiveStake function is called to decrease delegatorsEffectiveStake in:
unstake and delegate when currentValidatorStatus == VALIDATOR_STATUS_EXITED || delegation.status == DelegationStatus.PENDING
requestDelegationExit (in other code paths) — so in other cases it is not invoked in unstake/delegate
The condition currentValidatorStatus == VALIDATOR_STATUS_EXITED || delegation.status == DelegationStatus.PENDING does not account for the case where a delegator already called requestDelegationExit (which already decreased delegatorsEffectiveStake) and afterward the validator transitions to exited status. That sequence can cause _updatePeriodEffectiveStake to be invoked again in unstake/delegate because currentValidatorStatus == VALIDATOR_STATUS_EXITED, resulting in a double decrease.
1
Sequence that causes the double decrease (summary)
A delegator calls requestDelegationExit → this calls _updatePeriodEffectiveStake to decrease delegatorsEffectiveStake.
The validator later exits (validator status becomes EXITED).
The delegator then calls unstake or delegate. Because currentValidatorStatus == VALIDATOR_STATUS_EXITED, the code path in unstake/delegate again calls _updatePeriodEffectiveStake, decreasing delegatorsEffectiveStake a second time.
The double decrease can reduce delegatorsEffectiveStake below what remaining delegators rely on, causing subsequent exits/unstakes to fail and locking funds.
Impact Details
This special-case double decrease can cause delegatorsEffectiveStake to be excessively reduced, preventing some delegators from withdrawing their funds. An attacker might exploit this to lock funds or cause denial-of-withdrawals among honest delegators.
Add this test to the end of packages/contracts/test/unit/Stargate/Delegation.test.ts to reproduce the issue:
Test explanation:
Two delegator NFTs are active and delegated to the same validator.
NFT1 calls requestDelegationExit (which calls _updatePeriodEffectiveStake to reduce effective stake) but does not yet unstake.
The validator exits afterwards.
NFT1 calls unstake; due to validator status being EXITED, _updatePeriodEffectiveStake is called again during unstake, causing a double decrease.
The double decrease can make the effective stake zero, causing NFT2's unstake to revert.
The PoC logs (when running the test) show that during NFT1's unstake the delegatorsEffectiveStake is reduced twice.
Recommended mitigation (summary)
Ensure _updatePeriodEffectiveStake is not called a second time for a delegation that already had its effective stake decreased during requestDelegationExit.
Add and check a flag/state to indicate whether the effective stake has already been decreased for that delegation/period (or refine the condition to avoid double-decrement when delegator previously requested exit).
Carefully audit all code paths that call _updatePeriodEffectiveStake to ensure each delegation/period is decreased exactly once.