Liquid Staking

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

Unsafe `delegatecall` in `VaultControllerStrategy.sol

Summary

The VaultControllerStrategy.sol contract employs delegatecall to execute functions from an external vaultDepositController contract within its own context. This implementation poses a significant security risk, as it allows the external contract to manipulate the internal state of VaultControllerStrategy. If an attacker gains control over the vaultDepositController address, they can execute arbitrary code, leading to unauthorized state changes and potential fund theft.

Vulnerability Details

Unsafe Delegatecall in deposit Function

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();
}

Explanation:

The deposit function uses delegatecall to invoke the deposit function of the vaultDepositController contract. This means that any state changes made by the vaultDepositController will directly affect the storage of the VaultControllerStrategy contract. If vaultDepositController is malicious, it can manipulate critical variables such as totalDeposits or even reassign ownership.


Malicious Contract Exploiting Delegatecall

contract MaliciousVaultDepositController {
address public attacker;
constructor(address _attacker) {
attacker = _attacker;
}
function deposit(uint256 _amount, bytes calldata _data) external {
// Reassign ownership to attacker
(bool success, ) = msg.sender.call(
abi.encodeWithSignature("transferOwnership(address)", attacker)
);
require(success, "Ownership transfer failed");
// Transfer all tokens to attacker
IERC20Upgradeable token = IERC20Upgradeable(_data.toAddress(0));
uint256 balance = token.balanceOf(address(this));
token.transfer(attacker, balance);
}
}

Explanation:

This malicious contract overrides the deposit function expected by VaultControllerStrategy. Upon invocation via delegatecall, it performs the following actions:

  1. Ownership Transfer: Calls transferOwnership on the VaultControllerStrategy contract to transfer ownership to the attacker.

  2. Fund Drain: Transfers all tokens held by VaultControllerStrategy to the attacker's address.

By executing this contract through delegatecall, the attacker gains full control over the VaultControllerStrategy's state and funds.


Setting the Malicious Controller

// Assume attacker has ownership rights
vaultControllerStrategy.setVaultDepositController(address(maliciousController));

Explanation:

The attacker, having compromised the owner's credentials or through malicious intent, sets the vaultDepositController to the address of the MaliciousVaultDepositController contract. This setup is crucial for the exploit to succeed, enabling the malicious deposit function to manipulate the contract's state and funds.

Impact

  • Arbitrary Code Execution: The attacker can execute any code within the context of VaultControllerStrategy, allowing for extensive manipulation of the contract's state.

  • State Manipulation: Critical variables such as totalDeposits, totalPrincipalDeposits, and ownership can be altered, disrupting the contract's functionality.

  • Fund Theft: Unauthorized transfer of funds to the attacker's address can lead to significant financial losses.

  • Denial of Service: Manipulation of state variables can render the contract inoperative, affecting all stakeholders relying on it.

Tools Used

Manual Review

Recommendations

1. Eliminate the Use of delegatecall

Replace delegatecall with a standard external function call to ensure that external contracts cannot manipulate the internal state of VaultControllerStrategy.

(bool success, ) = vaultDepositController.call(
abi.encodeWithSelector(VaultDepositController.deposit.selector, _amount, _data)
);

2. Restrict vaultDepositController Updates

Implement stringent access controls and validation mechanisms to ensure that only trusted and audited contracts can be set as vaultDepositController.

  • Whitelist Trusted Contracts:

    Maintain a list of approved vaultDepositController addresses that have undergone thorough security audits.

    mapping(address => bool) public isTrustedController;
    function setVaultDepositController(address _vaultDepositController) external onlyOwner {
    require(isTrustedController[_vaultDepositController], "Controller not trusted");
    vaultDepositController = _vaultDepositController;
    }
  • Multi-Signature Approval:

    Require multiple signatures or confirmations before updating the vaultDepositController address to reduce the risk of unauthorized changes.

3. Immutable Controller Address

If the vaultDepositController does not need to change post-deployment, make its address immutable to prevent any future alterations.

address public immutable vaultDepositController;
constructor(address _vaultDepositController) {
vaultDepositController = _vaultDepositController;
}

4. Implement Reentrancy Guards

Protect functions that involve external calls with reentrancy guards to prevent reentrancy attacks, especially when dealing with token transfers.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract VaultControllerStrategy is Strategy, ReentrancyGuard {
// ...
function deposit(uint256 _amount, bytes calldata _data) external virtual onlyStakingPool nonReentrant {
// Function logic
}
function withdraw(uint256 _amount, bytes calldata _data) external onlyStakingPool nonReentrant {
// Function logic
}
}

5. Validate External Contract Interfaces

Ensure that any contract set as vaultDepositController adheres to a predefined and audited interface, preventing the execution of unintended functions.

interface IVaultDepositController {
function deposit(uint256 _amount, bytes calldata _data) external;
function withdraw(uint256 _amount, bytes calldata _data) external;
}
function setVaultDepositController(address _vaultDepositController) external onlyOwner {
require(
IVaultDepositController(_vaultDepositController).deposit.selector != bytes4(0),
"Invalid controller interface"
);
vaultDepositController = _vaultDepositController;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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