WETH unwrap fails due to incorrect token flow
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, andthe
_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↗.