Liquid Staking

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

Vulnerability in VaultDepositController: Delegatecall Failures in Deposit and Withdraw Functions Do Not Revert as Expected, Indirectly Putting Funds at Risk

Summary

A vulnerability was identified in the VaultDepositController.sol contract where the deposit and withdraw functions do not revert as expected when the delegatecall fails. Proper reverts are critical to maintaining the correct state and security of smart contracts.

Vulnerability Details

The deposit and withdraw functions employ delegatecall to invoke methods on another contract (VaultDepositController). When this delegatecall fails, the transaction must revert to ensure the correct state is maintained within the contract. However, the contract does not handle this scenario correctly, leading to potential unintended state changes.

The primary issue lies in the handling of the delegatecall return value. The delegatecall operation returns a boolean indicating success or failure. If the call fails and this boolean value is not properly checked, the contract may not revert, allowing the function to proceed despite the failure, thereby leaving the contract in an inconsistent state.

The affected functions are line 437-462 in VaultDepositController.sol:

function deposit(uint256 _amount, bytes calldata _data) external virtual onlyStakingPool {
if (vaultDepositController == address(0)) revert VaultDepositControllerNotSet();
(bool success,) = vaultDepositController.delegatecall(
abi.encodeWithSelector(VaultDepositController.deposit.selector, _amount, _data)
);
if (!success) revert DepositFailed();
}
function withdraw(uint256 _amount, bytes calldata _data) external onlyStakingPool {
if (vaultDepositController == address(0)) revert VaultDepositControllerNotSet();
(bool success,) = vaultDepositController.delegatecall(
abi.encodeWithSelector(VaultDepositController.withdraw.selector, _amount, _data)
);
if (!success) revert WithdrawalFailed();
}

POC

  • Copy the POC to the existing test test/linkStaking/vault-controller-strategy.test.ts

it('should revert deposit if delegatecall fails', async () => {
const { strategy, token } = await loadFixture(deployFixture);
const depositAmount = toEther(50);
console.log('Setting VaultDepositController to an invalid address to simulate delegatecall failure');
await strategy.setVaultDepositController('0x000000000000000000000000000000000000dEaD');
console.log(`Attempting to deposit ${depositAmount.toString()} ETH with failing delegatecall`);
try {
const initialBalance = await ethers.provider.getBalance(await strategy.getAddress());
console.log(`Initial ETH balance of strategy: ${initialBalance.toString()}`);
await strategy.deposit(depositAmount, encodeVaults([]));
const finalBalance = await ethers.provider.getBalance(await strategy.getAddress());
console.log(`Final ETH balance of strategy: ${finalBalance.toString()}`);
console.log(`Deposit of ${depositAmount.toString()} ETH did not revert as expected`);
} catch (error) {
console.log(`Deposit of ${depositAmount.toString()} ETH reverted with error:`, error);
if (error instanceof Error) {
console.log('Error message:', error.message);
console.log('Error stack:', error.stack);
}
await expect(Promise.reject(error)).to.be.revertedWithCustomError(strategy, 'DepositFailed()');
}
});
it('should revert withdraw if delegatecall fails', async () => {
const { strategy } = await loadFixture(deployFixture);
const withdrawAmount = toEther(50);
console.log('Setting VaultDepositController to an invalid address to simulate delegatecall failure');
await strategy.setVaultDepositController('0x000000000000000000000000000000000000dEaD');
console.log(`Attempting to withdraw ${withdrawAmount.toString()} ETH with failing delegatecall`);
try {
const initialBalance = await ethers.provider.getBalance(await strategy.getAddress());
console.log(`Initial ETH balance of strategy: ${initialBalance.toString()}`);
await strategy.withdraw(withdrawAmount, encodeVaults([]));
const finalBalance = await ethers.provider.getBalance(await strategy.getAddress());
console.log(`Final ETH balance of strategy: ${finalBalance.toString()}`);
console.log(`Withdraw of ${withdrawAmount.toString()} ETH did not revert as expected`);
} catch (error) {
console.log(`Withdraw of ${withdrawAmount.toString()} ETH reverted with error:`, error);
if (error instanceof Error) {
console.log('Error message:', error.message);
console.log('Error stack:', error.stack);
}
await expect(Promise.reject(error)).to.be.revertedWithCustomError(strategy, 'WithdrawalFailed()');
}
});

The PoC tests were designed to ensure that the deposit and withdraw functions revert correctly when the delegatecall fails. The tests presented the following output, which shows that the functions did not revert as expected:

Setting VaultDepositController to an invalid address to simulate delegatecall failure
Attempting to deposit 50000000000000000000 ETH with failing delegatecall
Initial ETH balance of strategy: 0
Final ETH balance of strategy: 0
Deposit of 50000000000000000000 ETH did not revert as expected
✔ should revert deposit if delegatecall fails
Setting VaultDepositController to an invalid address to simulate delegatecall failure
Attempting to withdraw 50000000000000000000 ETH with failing delegatecall
Initial ETH balance of strategy: 0
Final ETH balance of strategy: 0
Withdraw of 50000000000000000000 ETH did not revert as expected

Impact

The failure to revert on a failed delegatecall does not immediately put funds directly at risk but does create an indirect risk. The lack of proper revert handling can lead to improper state changes that disrupt protocol functionality or cause misaccounting, indirectly putting funds at risk. Thus, this vulnerability is classified as Medium severity.

Tools Used

  • Hardhat for deployment and testing.

Recommendations

  • Ensure Proper Reverts: Adjust the logic to ensure that the revert statements are correctly triggered on delegatecall failures. This can be done by reliably checking the return status of delegatecall.

Updates

Lead Judging Commences

inallhonesty Lead Judge 12 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.