Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Invalid

Lack of control access in OperatorVCS::removeVault function allows a vault to be deleted under unintended conditions

Summary

Operator Vault Controller Strategy contract is able to queue a vault for removal when the operator has been removed from the Chainlink staking contract using the queueVaultRemoval function. The vault can only be removed when the claim period is active for the vault contract in the Chainlink staking contract. However, since removeVault function is public and has no control access, anyone can remove a queued vault even when the required conditions are not met, resulting in unexpected behavior of the strategy system.

Vulnerability Details

When a vault operator has been removed from the Chainlink staking contract, the vault is queued for removal in the OperatorVCS contract via the queueVaultRemoval function. The vault is removed by calling the removeVault function only if the claim period is active in the Chainlink staking contract.

function queueVaultRemoval(uint256 _index) external {
address vault = address(vaults[_index]);
if (!IVault(vault).isRemoved()) revert OperatorNotRemoved();
for (uint256 i = 0; i < vaultsToRemove.length; ++i) {
if (vaultsToRemove[i] == vault) revert VaultRemovalAlreadyQueued();
}
vaultsToRemove.push(address(vaults[_index]));
// update group accounting if vault is part of a group
if (_index < globalVaultState.depositIndex) {
uint256 group = _index % globalVaultState.numVaultGroups;
uint256[] memory groups = new uint256[]();
groups[0] = group;
fundFlowController.updateOperatorVaultGroupAccounting(groups);
// if possiible, remove vault right away
@> if (vaults[_index].claimPeriodActive()) {
removeVault(vaultsToRemove.length - 1);
}
}
}

However, the removeVault function is public and has no control access, allowing any user to remove a vault that has been queued even if the claim period is not yet active.

@> function removeVault(uint256 _queueIndex) public {
address vault = vaultsToRemove[_queueIndex];
vaultsToRemove[_queueIndex] = vaultsToRemove[vaultsToRemove.length - 1];
vaultsToRemove.pop();
_updateStrategyRewards();
(uint256 principalWithdrawn, uint256 rewardsWithdrawn) = IOperatorVault(vault).exitVault();
totalDeposits -= principalWithdrawn + rewardsWithdrawn;
totalPrincipalDeposits -= principalWithdrawn;
uint256 numVaults = vaults.length;
uint256 index;
for (uint256 i = 0; i < numVaults; ++i) {
if (address(vaults[i]) == vault) {
index = i;
break;
}
}
for (uint256 i = index; i < numVaults - 1; ++i) {
vaults[i] = vaults[i + 1];
}
vaults.pop();
token.safeTransfer(address(stakingPool), token.balanceOf(address(this)));
}

Impact

Impact: High

A vault should only be removed when the required conditions are met.

Likelihood: Medium

Tools Used

Manual Review

Recommendations

Adding control access to the removeVault function will prevent users from remove queued vaults when the required conditions are not met.

- function removeVault(uint256 _queueIndex) public {
+ function removeVault(uint256 _queueIndex) public onlyOwner{
address vault = vaultsToRemove[_queueIndex];
vaultsToRemove[_queueIndex] = vaultsToRemove[vaultsToRemove.length - 1];
vaultsToRemove.pop();
_updateStrategyRewards();
(uint256 principalWithdrawn, uint256 rewardsWithdrawn) = IOperatorVault(vault).exitVault();
totalDeposits -= principalWithdrawn + rewardsWithdrawn;
totalPrincipalDeposits -= principalWithdrawn;
uint256 numVaults = vaults.length;
uint256 index;
for (uint256 i = 0; i < numVaults; ++i) {
if (address(vaults[i]) == vault) {
index = i;
break;
}
}
for (uint256 i = index; i < numVaults - 1; ++i) {
vaults[i] = vaults[i + 1];
}
vaults.pop();
token.safeTransfer(address(stakingPool), token.balanceOf(address(this)));
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.