Assessment reports>GTE>Medium findings>WETH unwrap fails due to incorrect token flow
Category: Coding Mistakes

WETH unwrap fails due to incorrect token flow

Medium Severity
Medium Impact
Medium Likelihood

Description

The executeRoute function allows executing multiple actions, postFillOrder and swapExactTokensForTokens, using the provided user funds and transferring the resulting tokens to the caller.

At the end of the executeRoute execution, if the resulting token is WETH and the isWrapping flag is set to true, the internal _handleUnwrap function is triggered. This function attempts to withdraw the final swap result from the WETH contract and transfer the corresponding amount of native tokens to the caller.

However, both _executeClobPostFillOrder and _executeUniV2SwapExactTokensForTokens functions already handle transferring the resulting tokens to the caller. The _executeClobPostFillOrder function withdraws the result of the last postFillOrder execution on behalf of the caller from the clobFactory, if it is the final swap in the sequence and the user has specified the settlement type as INSTANT. The _executeUniV2SwapExactTokensForTokens function uses msg.sender as the recipient if it is the final swap in the sequence.

As a result, during _handleUnwrap, the contract will attempt to transfer the resulting tokens to the caller a second time, which may lead to failing operations, since GTERouter balances are already depleted.

function executeRoute(
    [...]
    ) external payable override nonReentrant {
        [...]
        for (uint256 i = 0; i < hops.length; i++) {
            [...]
            if (currSelector == this.executeClobPostFillOrder.selector) {
                (route.prevAmountOut, route.nextTokenIn) = _executeClobPostFillOrder(route, hops[i]);
            } else if (currSelector == this.executeUniV2SwapExactTokensForTokens.selector) {
                (route.prevAmountOut, route.nextTokenIn) = _executeUniV2SwapExactTokensForTokens(route, hops[i]);
            } else {
                revert UnsupportedSelector();
            }

            route.prevSelector = currSelector;
        }

        [...]
        if (route.nextTokenIn == address(weth) && isWrapping) _handleUnwrap(route.prevAmountOut);
    }

Impact

Due to the redundant transfer logic in the executeRoute function, the router attempts to withdraw and transfer tokens to the caller a second time via _handleUnwrap, even though the resulting tokens have already been transferred during the execution of _executeClobPostFillOrder or _executeUniV2SwapExactTokensForTokens.

The design assumes that the router does not hold any additional token balance at the end of the action. Therefore, when _handleUnwrap tries to withdraw tokens, it is likely to revert, as there are no remaining funds to transfer. And this behavior will cause the entire transaction to fail.

Recommendations

Consider removing the _handleUnwrap logic from the executeRoute entirely and delegating the responsibility of unwrapping WETH to the user. Alternatively, consider adding specific logic to _executeClobPostFillOrder and _executeUniV2SwapExactTokensForTokens to handle cases where unwrapping is required.

In situations where the final token is WETH and the isWrapping flag is set to true,

  • the _executeUniV2SwapExactTokensForTokens should specify the recipient as the GTERouter itself instead of the caller, and

  • the _executeClobPostFillOrder function should withdraw tokens on behalf of the GTERouter contract instead of the caller.

This adjustment ensures that the router retains control of the resulting WETH, allowing _handleUnwrap to execute successfully without reverts.

Remediation

This issue has been acknowledged by Liquid Labs, Inc., and a fix was implemented in commit 200ce4a1.

Zellic © 2025Back to top ↑