A critical reentrancy vulnerability has been identified in the GMXProxy contract that could allow attackers to manipulate fund flows and drain assets during order execution. The vulnerability occurs due to improper ordering of state updates and external calls in the afterOrderExecution
function, allowing malicious contracts to re-enter and manipulate the contract's state before transaction finalization.
function afterOrderExecution(
bytes32 requestKey,
Order.Props memory order,
EventLogData memory eventData
) external override validCallback(requestKey, order) {
bytes32 positionKey = keccak256(
abi.encode(
address(this),
order.addresses.market,
order.addresses.initialCollateralToken,
order.flags.isLong
)
);
MarketPrices memory prices = getMarketPrices(order.addresses.market);
if (order.numbers.orderType != Order.OrderType.MarketSwap) {
address[] memory markets = new address[](2);
address[] memory tokens = new address[](2);
markets[0] = order.addresses.market;
markets[1] = order.addresses.market;
MarketProps memory marketInfo = gmxReader.getMarket(
address(dataStore),
order.addresses.market
);
tokens[0] = marketInfo.indexToken;
tokens[1] = marketInfo.shortToken;
try
gExchangeRouter.claimFundingFees(markets, tokens, perpVault)
returns (uint256[] memory claimedAmounts) {
emit ClaimPositiveFundingFees(tokens[0], claimedAmounts[0], tokens[1], claimedAmounts[1]);
} catch {
emit ClaimPositiveFundingFeeExecutionError(markets, tokens, perpVault);
}
}
if (order.numbers.orderType == Order.OrderType.Liquidation) {
if (eventData.uintItems.items[0].value > 0) {
IERC20(eventData.addressItems.items[0].value).safeTransfer(perpVault, eventData.uintItems.items[0].value);
}
if (eventData.uintItems.items[1].value > 0) {
IERC20(eventData.addressItems.items[1].value).safeTransfer(perpVault, eventData.uintItems.items[1].value);
}
IPerpetualVault(perpVault).afterLiquidationExecution();
} else if (msg.sender == address(adlHandler)) {
uint256 sizeInUsd = dataStore.getUint(keccak256(abi.encode(positionKey, SIZE_IN_USD)));
if (eventData.uintItems.items[0].value > 0) {
IERC20(eventData.addressItems.items[0].value).safeTransfer(perpVault, eventData.uintItems.items[0].value);
}
if (eventData.uintItems.items[1].value > 0) {
IERC20(eventData.addressItems.items[1].value).safeTransfer(perpVault, eventData.uintItems.items[1].value);
}
if (sizeInUsd == 0) {
IPerpetualVault(perpVault).afterLiquidationExecution();
}
} else {
address outputToken;
uint256 outputAmount;
if (
order.numbers.orderType == Order.OrderType.MarketSwap ||
order.numbers.orderType == Order.OrderType.MarketDecrease
) {
outputToken = eventData.addressItems.items[0].value;
outputAmount = eventData.uintItems.items[0].value;
}
IGmxProxy.OrderResultData memory orderResultData = IGmxProxy.OrderResultData(
order.numbers.orderType,
order.flags.isLong,
order.numbers.sizeDeltaUsd,
outputToken,
outputAmount,
queue.isSettle
);
IPerpetualVault(perpVault).afterOrderExecution(requestKey, positionKey, orderResultData, prices);
delete queue;
}
}
function afterOrderExecution(
bytes32 requestKey,
Order.Props memory order,
EventLogData memory eventData
) external override validCallback(requestKey, order) {
delete queue;
if (order.numbers.orderType != Order.OrderType.MarketSwap) {
try gExchangeRouter.claimFundingFees(markets, tokens, perpVault) {
emit ClaimPositiveFundingFees(tokens[0], claimedAmounts[0], tokens[1], claimedAmounts[1]);
} catch {
emit ClaimPositiveFundingFeeExecutionError(markets, tokens, perpVault);
}
}
}
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract GmxProxy is IGmxProxy, IOrderCallbackReceiver, Initializable, Ownable2StepUpgradeable, ReentrancyGuard {
function afterOrderExecution(
bytes32 requestKey,
Order.Props memory order,
EventLogData memory eventData
) external override validCallback(requestKey, order) nonReentrant {
}
}