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

Some perpetual vaults lacks functionality for handling Native Tokens and WNT, remaining stuck in the contract

Vulnerability details

Inside the GMX protocol contract ExecuteOrderUtils.sol, after making the callback to GmxProxy.sol::afterOrderExecution() of the Gamma protocol, it initiates the process of paying the execution fee to the GMX keeper for the transaction and refunds any excess amount to the callback contract (GmxProxy.sol).
ExecuteOrderUtils.sol::executeOrder():

function executeOrder(BaseOrderUtils.ExecuteOrderParams memory params) external {
...
@> GasUtils.payExecutionFee(
params.contracts.dataStore,
params.contracts.eventEmitter,
params.contracts.orderVault,
params.key,
params.order.callbackContract(),
params.order.executionFee(),
params.startingGas,
GasUtils.estimateOrderOraclePriceCount(params.order.swapPath().length),
params.keeper,
params.order.receiver()
);
...
}

GasUtils.sol::payExecutionFee()

EventUtils.EventLogData memory eventData;
// Refund is tried to send to the callback contract with the native token (e.g., ETH).
@> cache.refundWasSent = CallbackUtils.refundExecutionFee(dataStore, key, callbackContract, cache.refundFeeAmount, eventData);
if (cache.refundWasSent) {
emitExecutionFeeRefundCallback(eventEmitter, callbackContract, cache.refundFeeAmount);
} else {
// If not try send it to the receiver (PerpetualVault.sol) instead of the callback contract
@> TokenUtils.sendNativeToken(dataStore, refundReceiver, cache.refundFeeAmount);
emitExecutionFeeRefund(eventEmitter, refundReceiver, cache.refundFeeAmount);
}

As you can see, if the refund transaction fails due to insufficient gas or other errors, the refund is made to the receiver() address of the order object (refundReceiver == PerpetualVault).

Inside TokenUtils.sol::sendNativeToken(), it first attempts to send the native token; if that fails, it will send the Wrapped Native Token (WNT) to the refund receiver (PerpetualVault).

TokenUtils.sol::sendNativeToken()

bool success;
// use an assembly call to avoid loading large data into memory
// input mem[in…(in+insize)]
// output area mem[out…(out+outsize))]
assembly {
success := call(
gasLimit, // gas limit
receiver, // receiver
amount, // value
0, // in
0, // insize
0, // out
0 // outsize
)
}
if (success) { return; }
// if the transfer failed, re-wrap the token and send it to the receiver
@> depositAndSendWrappedNativeToken(
dataStore,
receiver,
amount
);

And according the integration notes of GMX contracts:

  • ETH transfers are sent with NATIVE_TOKEN_TRANSFER_GAS_LIMIT for the gas limit, if the transfer fails due to insufficient gas or other errors, the ETH is sent as WETH instead

  • Accounts may receive ETH for ADLs / liquidations, if the account cannot receive ETH then WETH would be sent instead

As the Perpetual Vault contract lacks functionality for handling the Native Token or WNT, perpetual vaults whose market tokens are neither the Native Token nor WNT as collateralToken or indexToken will have the refund funds stuck in the contract.

For instance, the market LINK/USD don't handle any WETH or ETH as indexToken or collateralToken, when the vault receive some funds in Native or WNT tokens because of some refund, then those funds will be stuck in the contract. It also won't account as idle funds because is taking the balance of the collateralToken(USDC):

function _isFundIdle() internal view returns (bool) {
if (collateralToken.balanceOf(address(this)) >= minDepositAmount) {
return true;
} else {
return false;
}
}

So we can say that in some perpetual vaults, the refund funds will be locked in the contract.

Impact

Funds stucked into the contract.

Root Cause

Not having functionality for handling the refund amounts of Native WNT tokens.

Recommendations

Add some function to transfer the native tokens to the Gmx Proxy and withdraw the WNT as the Gmx proxy don't have functionality for handle WNT as well.

Updates

Lead Judging Commences

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

invalid_gmx_send_WETH_fees

`TokenUtils.sol::sendNativeToken()` has no reason to fail since there is a `receive` function without any instruction in the GmxProxy. It’s the simpliest and cheapest transfer possible. Good finding, but there is no likelihood.

Support

FAQs

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

Give us feedback!