Steadefi

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

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

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.

Updates

Lead Judging Commences

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.

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

Not complete reverse action for withdraw failure

Support

FAQs

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