DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Slippage loss due to hardcoding `minOutputAmount: 0` as zero for creating all MarketDecrease Order

Summary

The settle(...) function of GmxProxy.solhardcodes the minOutputAmount: 0 to zero when creating the param struct to call the gExchangeRouter.createOder(params)function of GMX.

The comment in front of the hardcoded minOutputAmount: 0assumes that the minOutputAmountis not used on the GMX Market decrease logic. Contrary to this assumption, the minOutputAmountis used for validation after the swap in the GMX code with the [_validateOutputAmount(...)]() function.

This can lead to loss of asset through slippage loss.

Even quoting from the GMX docs, NOTE: For decrease orders this is the minimum USD value, USD is used in this case because it is possible for decrease orders to have two output tokens

here is the full quote from:

>minOutputAmount: For swap orders this is the minimum token output amount. For increase orders this is the minimum token amount after the initialCollateralDeltaAmount is swapped through the swapPath. NOTE: For decrease orders this is the minimum USD value, USD is used in this case because it is possible for decrease orders to have two output tokens, one being the profit token and the other being the withdrawn collateral token

Vulnerability Details

Zero is hardcoded as the minOutputAmountin the settle(...)function.

The comment assumes that the minOutputAmountis not used in the GMX logic for Market Decrease. However the minOutputAmountis used to validate the output amount to prevent slippage loss in the [_validateOutputAmount(...)]() function.

function settle(
IGmxProxy.OrderData memory orderData
) external returns (bytes32) {
require(msg.sender == perpVault, "invalid caller");
uint256 positionExecutionFee = getExecutionGasLimit(
Order.OrderType.MarketDecrease,
orderData.callbackGasLimit
) * tx.gasprice;
require(
address(this).balance >= positionExecutionFee,
"insufficient eth balance"
);
gExchangeRouter.sendWnt{value: positionExecutionFee}(
orderVault,
positionExecutionFee
);
CreateOrderParamsAddresses memory paramsAddresses = CreateOrderParamsAddresses({
receiver: perpVault,
cancellationReceiver: address(perpVault),
callbackContract: address(this),
uiFeeReceiver: address(0),
market: orderData.market,
initialCollateralToken: orderData.initialCollateralToken,
swapPath: new address[](0)
});
CreateOrderParamsNumbers memory paramsNumber = CreateOrderParamsNumbers({
sizeDeltaUsd: 0,
initialCollateralDeltaAmount: 1,
triggerPrice: 0,
acceptablePrice: 0,
executionFee: positionExecutionFee,
callbackGasLimit: orderData.callbackGasLimit,
@> minOutputAmount: 0, // this param is used when swapping. is not used in opening position even though swap involved.
validFromTime: 0
});
CreateOrderParams memory params = CreateOrderParams({
addresses: paramsAddresses,
numbers: paramsNumber,
orderType: Order.OrderType.MarketDecrease,
decreasePositionSwapType: Order
.DecreasePositionSwapType
.SwapPnlTokenToCollateralToken,
isLong: orderData.isLong,
shouldUnwrapNativeToken: false,
autoCancel: false,
referralCode: referralCode
});
bytes32 requestKey = gExchangeRouter.createOrder(params);
queue.requestKey = requestKey;
queue.isSettle = true;
return requestKey;
}

Let's look at the GMX Market decrease implementation

File: https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/order/DecreaseOrderUtils.sol#L99C9-L120C15
try params.contracts.swapHandler.swap(
SwapUtils.SwapParams(
params.contracts.dataStore,
params.contracts.eventEmitter,
params.contracts.oracle,
Bank(payable(order.market())),
params.key,
result.outputToken,
result.outputAmount,
params.swapPathMarkets,
0,
order.receiver(),
order.uiFeeReceiver(),
order.shouldUnwrapNativeToken()
)
) returns (address tokenOut, uint256 swapOutputAmount) {
_validateOutputAmount(
params.contracts.oracle,
tokenOut,
swapOutputAmount,
order.minOutputAmount()
);

And the below is the implementation of _validateOutputAmount(...)

// note that minOutputAmount is treated as a USD value for this validation
function _validateOutputAmount(
Oracle oracle,
address outputToken,
uint256 outputAmount,
uint256 minOutputAmount
) internal view {
uint256 outputTokenPrice = oracle.getPrimaryPrice(outputToken).min;
uint256 outputUsd = outputAmount * outputTokenPrice;
if (outputUsd < minOutputAmount) {
revert Errors.InsufficientOutputAmount(outputUsd, minOutputAmount);
}
}

Impact

Loss of asset due to slippage loss from sandwich attack.

Tools Used

[_validateOutputAmount(...)]() function of GMX decrease order logic

Recommendations

Consider passing implementing the minOutputAmount input on the settle(...) function like it is done for the createOder(...)function instead of hardcoding it to zero. Note that this minOutputAmountis in USD.

Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_gmx_increase/decrease_no_slippage

acceptablePrice does that job for increase/decrease positions. https://github.com/gmx-io/gmx-synthetics/blob/caf3dd8b51ad9ad27b0a399f668e3016fd2c14df/contracts/order/BaseOrderUtils.sol#L276C49-L276C66

Support

FAQs

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