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

Gamma claims collateral rebates only in `collateralToken`, which leaves part of the rebates never claimed

Description:

GMX has a maximum cap of negative price impact on decrease orders. If a trade is closed with a price impact higher than this percentage, then the additional impact would become claimable after a few days.

In the current implementation of PerpetualVault::claimCollateralRebates function, only collateralToken is added to the tokens array and sent to GMX's ExchangeRouter::claimCollateral function as a parameter.

In the code snippet below we see that only one type of token is added to the tokens array:

function claimCollateralRebates(uint256[] memory timeKeys) external nonReentrant {
_noneFlow();
_onlyKeeper();
address[] memory markets = new address[](1);
markets[0] = market;
address[] memory tokens = new address[](1);
@> tokens[0] = address(collateralToken);
IGmxProxy(gmxProxy).claimCollateralRebates(markets, tokens, timeKeys, treasury);
}

This is a problem, because negative price impact above the maximum value can occur in both long and short token. During MarketDecrease order execution, if the negative price impact is above the maximum limit, GMX adds the number of tokens above the limit to the claimable collateral pool and after few days it becomes claimable and the owner of the position who experienced the negative price impact can call ExchangeRouter::claimCollateral function to receive the rebate. During the claiming process, this function in GMX's MarketUtils gets called:

function validateMarketTokenBalance(DataStore dataStore, Market.Props memory market) public view {
@> validateMarketTokenBalance(dataStore, market, market.longToken);
if (market.longToken == market.shortToken) {
return;
}
@> validateMarketTokenBalance(dataStore, market, market.shortToken);
}

Notice, that this function validates market token balance of both long and short tokens, which means that negative price impact can be claimed both in long token and short token of the market (e.g. WETH and USDC).

The complete rebate claiming execution flow looks like this:

PerpetualVault::claimCollateralRebates -> GmxProxy::claimCollateralRebates -> ExchangeRouter::claimCollateral -> MarketUtils::claimCollateral (calculates amountToBeClaimed and transfers out the tokens) -> MarketUtils::validateMarketTokenBalance (ensures market token balance of both long and short token is enough, else - reverts)

Impact:

If GMX holds some of the claimable collateral in the form of long tokens (e.g. WETH), Gamma's current implementation will not claim it. The unclaimed long tokens will stay in GMX’s claimable collateral pool until properly requested. This means Gamma is leaving some rebates unclaimed, leading to inefficiencies.

Proof of concept:

In DecreasePositionCollateralUtils::processCollateral function in lines 433 - 473, we can see proof that negative price impact over the maximum limit can occur and be paid in both long and short tokens:

// pay for price impact diff
if (values.priceImpactDiffUsd > 0) {
@> (values, collateralCache.result) = payForCost(
params,
values,
cache.prices,
cache.collateralTokenPrice,
values.priceImpactDiffUsd
);
@> if (collateralCache.result.amountPaidInCollateralToken > 0) {
MarketUtils.incrementClaimableCollateralAmount(
params.contracts.dataStore,
params.contracts.eventEmitter,
params.market.marketToken,
@> params.position.collateralToken(),
params.order.account(),
collateralCache.result.amountPaidInCollateralToken
);
}
@> if (collateralCache.result.amountPaidInSecondaryOutputToken > 0) {
MarketUtils.incrementClaimableCollateralAmount(
params.contracts.dataStore,
params.contracts.eventEmitter,
params.market.marketToken,
@> values.output.secondaryOutputToken,
params.order.account(),
collateralCache.result.amountPaidInSecondaryOutputToken
);
}
if (collateralCache.result.remainingCostUsd > 0) {
return handleEarlyReturn(
params,
values,
fees,
collateralCache,
"diff"
);
}
}

Source: https://github.com/gmx-io/gmx-synthetics/blob/b8fb11349eb59ae48a1834c239669d4ad63a38b5/contracts/position/DecreasePositionCollateralUtils.sol#L433

Note, that:

  1. values.priceImpactDiffUsd is the difference between maximum negative price impact and the actual price impact that was calculated (Proof: https://github.com/gmx-io/gmx-synthetics/blob/b8fb11349eb59ae48a1834c239669d4ad63a38b5/contracts/position/DecreasePositionCollateralUtils.sol#L91)

  2. Paying for price impact difference occurs in DecreasePositionCollateralUtils::payForCost function: https://github.com/gmx-io/gmx-synthetics/blob/b8fb11349eb59ae48a1834c239669d4ad63a38b5/contracts/position/DecreasePositionCollateralUtils.sol#L521

  3. MarketUtils::incrementClaimableCollateralAmount function is used for increasing the collateral amount the owner of the position can claim, and as we can see, it is called 2 times in the above code snippet, one time for collateral token, second time for secondaryOutputToken (e.g. WETH).

For further proof, take a look at this comment from lines 476 - 482 in DecreasePositionCollateralUtils::processCollateral function:

// the priceImpactDiffUsd has been deducted from the output amount or the position's collateral
// to reduce the chance that the position's collateral is reduced by an unexpected amount, adjust the
// initialCollateralDeltaAmount by the priceImpactDiffAmount
// this would also help to prevent the position's leverage from being unexpectedly increased
//
@> // note that this calculation may not be entirely accurate since it is possible that the priceImpactDiffUsd
@> // could have been paid with one of or a combination of collateral / outputAmount / secondaryOutputAmount

Source: https://github.com/gmx-io/gmx-synthetics/blob/b8fb11349eb59ae48a1834c239669d4ad63a38b5/contracts/position/DecreasePositionCollateralUtils.sol#L476

Recommended Mitigation:

Change the length of both arrays to 2, add the index token to the tokens array and add market to the second slot in markets array, similar to the setup before calling claimFundingFees in GmxProxy::afterOrderExecution function.

Example fix:

function claimCollateralRebates(uint256[] memory timeKeys) external nonReentrant {
_noneFlow();
_onlyKeeper();
- address[] memory markets = new address[](1);
+ address[] memory markets = new address[](2);
markets[0] = market;
+ markets[1] = market;
- address[] memory tokens = new address[](1);
+ address[] memory tokens = new address[](2);
tokens[0] = address(collateralToken);
+ tokens[1] = address(indexToken);
IGmxProxy(gmxProxy).claimCollateralRebates(markets, tokens, timeKeys, treasury);
}
Updates

Lead Judging Commences

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

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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

Give us feedback!