Relevant GitHub Links
Summary
- Depositor funds are at severe risk due to front-run attacks and slippage mishandling. An
emergencyClose()
can lead to complete investment loss, The same issue observed in other functions (processDepositFailureLiquidityWithdrawal and processWithdraw) but with less impact.
Vulnerability Details
- The
emergencyClose()
function repays all debt to thelendingVault
contract, then sets the status toclosed
. Subsequently, users who deposited into the strategy contract can withdraw their deposits.
- If the contract balance of one token is insufficient to cover its debt, the contract swaps the other token for the necessary amount using
swapTokensForExactTokens
.
plain textfunction emergencyClose(GMXTypes.Store storage self, uint256 deadline) external { // some code .. //... // Repay all borrowed assets; 1e18 == 100% shareRatio to repay GMXTypes.RepayParams memory _rp; (_rp.repayTokenAAmt, _rp.repayTokenBAmt) = GMXManager.calcRepay(self, 1e18); (bool _swapNeeded, address _tokenFrom, address _tokenTo, uint256 _tokenToAmt) = GMXManager.calcSwapForRepay(self, _rp); if (_swapNeeded) { ISwap.SwapParams memory _sp; _sp.tokenIn = _tokenFrom; _sp.tokenOut = _tokenTo; _sp.amountIn = IERC20(_tokenFrom).balanceOf(address(this)); _sp.amountOut = _tokenToAmt; _sp.slippage = self.minSlippage; _sp.deadline = deadline; // @audit-issue : this can be front-run to return >> GMXManager.swapTokensForExactTokens(self, _sp); } // more code .... //..... }
- Notice that the
_sp
struct includes the_sp.slippage
parameter. Here,_sp.amountIn
represents the entire contract balance oftokenFrom
, while_sp.amountOut
represent only the necessary amount to settle the debt oftokenTo
. This function is vulnerable to front-running if the contract'stokenFrom
balance exceeds the required amount to swap foramountOut
oftokenTo
. In such cases, all remaining tokens from_tokenFrom
can be stolen in a front-run attack, as the_sp.slippage
parameter has no effect in this scenario and we can see that here :
- the contract first call
GMXManager.swapTokensForExactTokens(self, _sp)
plain textfunction swapTokensForExactTokens(GMXTypes.Store storage self, ISwap.SwapParams memory sp) external returns (uint256) { if (sp.amountIn > 0) { return GMXWorker.swapTokensForExactTokens(self, sp); } else { return 0; } }
- then call
GMXWorker.swapTokensForExactTokens(self, sp);
plain textfunction swapTokensForExactTokens(GMXTypes.Store storage self, ISwap.SwapParams memory sp) external returns (uint256) { IERC20(sp.tokenIn).approve(address(self.swapRouter), sp.amountIn); return self.swapRouter.swapTokensForExactTokens(sp); }
- then call
self.swapRouter.swapTokensForExactTokens(sp)
plain textfunction swapTokensForExactTokens(ISwap.SwapParams memory sp) external returns (uint256) { IERC20(sp.tokenIn).safeTransferFrom(msg.sender, address(this), sp.amountIn); IERC20(sp.tokenIn).approve(address(router), sp.amountIn); ISwapRouter.ExactOutputSingleParams memory _eosp = ISwapRouter.ExactOutputSingleParams({ tokenIn: sp.tokenIn, tokenOut: sp.tokenOut, fee: fees[sp.tokenIn][sp.tokenOut], recipient: address(this), deadline: sp.deadline, amountOut: sp.amountOut, amountInMaximum: sp.amountIn, sqrtPriceLimitX96: 0 }); uint256 _amountIn = router.exactOutputSingle(_eosp); // Return sender back any unused tokenIn IERC20(sp.tokenIn).safeTransfer(msg.sender, IERC20(sp.tokenIn).balanceOf(address(this))); IERC20(sp.tokenOut).safeTransfer(msg.sender, IERC20(sp.tokenOut).balanceOf(address(this))); return _amountIn; }
- notice that the function
swapTokensForExactTokens
callsexactOutputSingle
from uniswap router and the_sp.slipage
have no effect .
- the vault faces a significant risk, losing all remaining assets, which primarily consist of depositors' funds. This vulnerability is exacerbated by the Vault contract withdrawing liquidity from GMX without performing any swaps during the
emergencyPause
action, leaving withdrawed liquidity in terms of both tokens and making swaps inemergencyClose
will be almost always necessary.
- The same vulnerability also exists in the processDepositFailureLiquidityWithdrawal and processWithdraw functions. However, the impact in the
emergencyClose()
function is significantly more severe compared to these cases. but the vulnerability is the same.
POC
consider the following sinario :
assume that The vault is a long vault has a debt of
10000
tokenA
.- When the contract paused, it withdrawed
5000 tokenA
and10000 tokenB
. so The contract still needs5000 tokenA
to cover all the debt.
- The contract attempts to swap a maximum of
tokenB
to obtain the required5000 tokenA
. Typically, this swap requires6000 tokenB
to yield5000 tokenA
. After the swap, there are4000 tokenB
left, representing users deposits. These deposits can only be redeemed after the debt is settled.
- The
amountIn
parameter is set to10000 tokenB
. A malicious actor exploit this by front-running the swap, inflating the price. The contract ends up swapping10000 tokenB
for5000 tokenA
and repays the debt.
- After the swap and debt repayment, the vault's balance becomes zero, leaving no funds to cover user deposits.
NOTICE
Depositors' funds in this case are always will be in terms oftokenFrom
(or tokenB in our example), ensuring consistent losses. - In a Neutral strategy, the attacker must keep enoughtokenFrom
to the contract to paytokenFrom
debt to prevent transaction from revert and only take all the depositors funds.
Impact
- in
emergencyClose()
all depositors loses all thier deposits .
- in processDepositFailureLiquidityWithdrawal one user lose all his deposit (the failed deposit).
- in processWithdraw user have the ability to set the
minTokenOut
he will get but still possible that the user lose his funds if he misspass the right amount.
Tools Used
vs code
manual review
Recommendations
- after the swap. make sure that
amountIn
swapped foramountOut
Get swapped for a reasonable price(in a specific buffer) using chainlink Oracle and if not revert.