Summary
The GMXEmergency utilizes an unbounded while loop for subtracting the shareAmt from _userShareBalance
. This presents a security concern, as unbounded loops can lead to excessive gas consumption and unintended issues in cases where there are a large number of shares to withdraw.
-The GMXCompound code also uses a while loop for subtracting _tokenInAmt
from the balance of cp.tokenIn
. This loop might potentially lead to issues if there is a large number of tokens to swap, consuming excessive gas or leading to unintended outcomes like blocking contract functions
Vulnerability Details
function emergencyWithdraw(
GMXTypes.Store storage self,
uint256 shareAmt
) external {
uint256 _userShareBalance = IERC20(address(self.vault)).balanceOf(msg.sender);
unchecked {
if (_userShareBalance - shareAmt < DUST_AMOUNT) {
shareAmt = _userShareBalance;
}
}
}
-
The code employs an unbounded while loop, which iteratively reduces shareAmt from _userShareBalance
until a condition is met. The concern is that in cases with a very high _userShareBalance
or shareAmt
, the loop may consume a significant amount of gas leading to out-of-gas errors.
-
In a scenario where a user possesses an extremely high _userShareBalance and intends to withdraw a substantial shareAmt, the gas cost of the loop can increase significantly, potentially leading to failed or reverted transactions due to excessive gas consumption. -This situation could also be exploited by malicious users to intentionally trigger out-of-gas errors in an attempt to disrupt the contract's normal operation (DoS), BLOCKING others from withdrawing
Impact
Gas Overconsumption: The contract may consume a considerable amount of gas when processing high share withdrawals, leading to increased transaction costs and potential transaction failures.
Disrupted Functionality: Extremely high shareAmt or _userShareBalance values could disrupt or block the normal operation of the contract, resulting in failed or reverted transactions.
Tools Used
Manual code analysis
foundry
Recommendations
Limit Loop Execution: Introduce a maximum iteration limit for the loop or use other methods to restrict the loop execution to a reasonable number of iterations.
Here is code fix
pragma solidity 0.8.21;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ISwap } from "../../interfaces/swap/ISwap.sol";
import { GMXTypes } from "./GMXTypes.sol";
import { GMXChecks } from "./GMXChecks.sol";
import { GMXManager } from "./GMXManager.sol";
* @title GMXEmergency
* @author Steadefi
* @notice Re-usable library functions for emergency operations for Steadefi leveraged vaults
*/
library GMXEmergency {
using SafeERC20 for IERC20;
uint256 public constant SAFE_MULTIPLIER = 1e18;
uint256 public constant DUST_AMOUNT = 1e17;
+ uint256 public constant MAX_ITERATIONS = 100;
event EmergencyPause();
event EmergencyResume();
event EmergencyClose(
uint256 repayTokenAAmt,
uint256 repayTokenBAmt
);
event EmergencyWithdraw(
address indexed user,
uint256 sharesAmt,
address assetA,
uint256 assetAAmt,
address assetB,
uint256 assetBAmt
);
* @notice @inheritdoc GMXVault
* @param self GMXTypes.Store
*/
function emergencyPause(
GMXTypes.Store storage self
) external {
self.refundee = payable(msg.sender);
GMXTypes.RemoveLiquidityParams memory _rlp;
_rlp.lpAmt = self.lpToken.balanceOf(address(this));
_rlp.executionFee = msg.value;
GMXManager.removeLiquidity(
self,
_rlp
);
self.status = GMXTypes.Status.Paused;
emit EmergencyPause();
}
* @notice @inheritdoc GMXVault
* @param self GMXTypes.Store
*/
function emergencyWithdraw(
GMXTypes.Store storage self,
uint256 shareAmt
) external {
uint256 _userShareBalance = IERC20(address(self.vault)).balanceOf(msg.sender);
unchecked {
if (_userShareBalance - shareAmt < DUST_AMOUNT) {
shareAmt = _userShareBalance;
}
}
GMXChecks.beforeEmergencyWithdrawChecks(self, shareAmt);
+ for (uint256 i = 0; i < MAX_ITERATIONS; i++) {
if (shareAmt == 0) {
break; // Exit the loop once shareAmt is fully processed
}
uint256 portion = shareAmt / MAX_ITERATIONS;
if (portion == 0) {
portion = shareAmt;
}
uint256 _shareRatio = portion * SAFE_MULTIPLIER / IERC20(address(self.vault)).totalSupply();
uint256 _withdrawAmtTokenA = _shareRatio * self.tokenA.balanceOf(address(this)) / SAFE_MULTIPLIER;
uint256 _withdrawAmtTokenB = _shareRatio * self.tokenB.balanceOf(address(this)) / SAFE_MULTIPLIER;
self.tokenA.safeTransfer(msg.sender, _withdrawAmtTokenA);
self.tokenB.safeTransfer(msg.sender, _withdrawAmtTokenB);
shareAmt -= portion;
}
emit EmergencyWithdraw(
msg.sender,
shareAmt,
address(self.tokenA),
_withdrawAmtTokenA,
address(self.tokenB),
_withdrawAmtTokenB
);
}
}