Relevant GitHub Links
Summary
- If adding liquidity to
GMX
get canceled after a failed withdrawal, the contract stuck in thewithdraw_failed
status.
Vulnerability Details
- The status
withdraw_failed
is set when the vault successfully withdrew from GMX, but the callback failed during theprocessWithdraw()
checks inside thetry
call, as seen here:
plain textfunction 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 :
plain textfunction 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
:plain textfunction 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 ofborrow 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 thelending
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 thecallback
contract, implement proper handling for canceled liquidity addition when the status iswithdraw_Failed
.