medium

Strategy Vault stuck at `withdraw_failed` status if the deposit to `GMX` get ...

Contest
Reward

Total

564.75 USDC

Selected
564.75 USDC
Selected Submission

Strategy Vault stuck at withdraw_failed status if the deposit to GMX get Cancelled

Severity

High Risk

Relevant GitHub Links

https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/strategy/gmx/GMXWithdraw.sol

Summary

  • If adding liquidity to GMX get canceled after a failed withdrawal, the contract stuck in the withdraw_failed status.

Vulnerability Details

  • The status withdraw_failed is set when the vault successfully withdrew from GMX, but the callback failed during the processWithdraw() checks inside the try call, as seen here:
  function processWithdraw(GMXTypes.Store storage self) external {

        GMXChecks.beforeProcessWithdrawChecks(self);// revert if status not withdraw
        try GMXProcessWithdraw.processWithdraw(self) {
          ////... some code
        } catch (bytes memory reason) {
            // withdraw failed only can be set here .
>>          self.status = GMXTypes.Status.Withdraw_Failed;
            emit WithdrawFailed(reason);
        }
    }

the keeper listens to events in this scenario, it calls the processWithdrawFailure() function. This function reborrows the same amount's :

function processWithdrawFailure(GMXTypes.Store storage self, uint256 slippage, uint256 executionFee) external {
    GMXChecks.beforeProcessAfterWithdrawFailureChecks(self);

    // Re-borrow assets based on the repaid amount
>>    GMXManager.borrow(
        self, self.withdrawCache.repayParams.repayTokenAAmt, self.withdrawCache.repayParams.repayTokenBAmt
    );
    //......
}

it then add liquidity to gmx :

  function processWithdrawFailure(GMXTypes.Store storage self, uint256 slippage, uint256 executionFee) external {
  GMXChecks.beforeProcessAfterWithdrawFailureChecks(self);

    // Re-borrow assets based on the repaid amount
    GMXManager.borrow(
        self, self.withdrawCache.repayParams.repayTokenAAmt, self.withdrawCache.repayParams.repayTokenBAmt
    );
    // .... some code

    // Re-add liquidity with all tokenA/tokenB in vault
>>      self.withdrawCache.depositKey = GMXManager.addLiquidity(self, _alp);
}

The problem arises when adding liquidity to GMX is canceled, there is no mechanism to handle this scenario when the status is withdraw_failed. In this case, the callback will revert, as seen here, leaving the tokens from the first withdrawal + borrowed tokens stuck in the contract with the withdraw_failed status.

In this situation, the only available action to interact with the contract is to call processWithdrawFailure() again (or emergencyPause).

Even if the keeper can call this without any event listening, doing so exacerbates the situation. It results in a loop of borrow more => add liquidity => get canceled, continuing until there are no more funds to borrow or the keeper runs out of gas.

  • Another issue arises when there is insufficient funds in the lending contract for borrowing, as this function does not check the capacity before borrowing. This results in repeated reverting transactions since the amount the keeper want to borrow is more then the amount available in the lending contract.

Impact

  • Renders users unable to withdraw or deposit funds, halting essential interactions with the protocol.
  • Poses a risk of failing to rebalance the contract, potentially resulting in bad debt accumulation.

Tools Used

vs code.

Recommendations

  • In the afterWithdrawalCancellation() function of the callback contract, implement proper handling for canceled liquidity addition when the status is withdraw_Failed.