# Uniswap bug causes loss of ETH There is a bug in Uniswap v3 that can cause a loss of any amount of ETH in certain transactions involving pools with inadequate liquidity (not due to slippage or price impact). This bug has already resulted in the loss of ~23 ETH for a Uniswap user on Arbitrum. It also presents a currently unexploited MEV opportunity. Uniswap has been notified and refused to address this bug. ## Background Some background on Uniswap v3 is required to fully understand the mechanics of this bug: 1. End users use a v3 router contract to interact with Uniswap. Swaps are done through this router contract. 2. If the input token of the swap is the native asset (ETH), it is sent natively to the router contract in the swap transaction. 3. Swaps usually require the caller to call multiple router contract methods in sequence, and the router contract has a `multicall()` method to enable this. 4. These `multicall()` calls are constructed by a front-end like the Uniswap interface. 5. Sometimes, not all of the ETH sent to the router contract is actually needed in the swap. Because of this, the last method called in `multicall()` is usually `refundETH()`, which transfers **all** ETH present in the router contract to the caller (`msg.sender`): ```solidity function refundETH() external payable override { if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance); } ``` 6. There are 2 types of swaps in Uniswap: exact input and exact output. With exact input transactions, you specify the exact amount of input token you'd like to swap with. With exact output transactions, you specify the exact amount of the output token you'd like to receive. ## The bug It is obvious how important it is to **always** call the `refundETH()` method at the end of any transaction with the router contract. If you fail to call `refundETH()` as the last method in a `multicall()`, you may end up leaving some unused ETH in the router contract. Due to the way the `refundETH()` function is written (see above), the next caller of `refundETH()` will receive any ETH you left in the contract. Unfortunately, the official Uniswap interface doesn't always include a call to `refundETH()`. In certain real-world scenarios, this may result in a user inadvertently leaving ETH in the router contract for someone else (or an MEV bot) to claim. The most obvious case in which to include a call to `refundETH()` is when doing an exact output swap using the native token as the input token. Since you don't know exactly how much of the input token is required, there will likely be some of it left over when the swap is completed. The user would have to reclaim this by calling `refundETH()`. This exact case is addressed by the [router SDK](https://github.com/Uniswap/router-sdk/blob/7d989fbe285abf32a63c602221cd136651e39103/src/swapRouter.ts#L361), which is used by the Uniswap interface: ```typescript // must refund when paying in ETH, but with an uncertain input amount if (inputIsNative && sampleTrade.tradeType === TradeType.EXACT_OUTPUT) { calldatas.push(Payments.encodeRefundETH()) } ``` The problem is that is the **only** case in which `refundETH()` is included in the `multicall()` transaction, but there are other instances in which you may have some ETH left in the router contract after your swap is complete. It is definitely possible to have ETH left over after the swap in an exact input swap as well. This happens when there is inadequate liquidity in the pool relative to the size of your swap. In that case, the router SDK does not include a call to `refundETH()` and ETH is left in the router contract for someone else to claim. No warning of any kind is shown to the user by the interface. ## Real world occurrences This is not a theoretical bug, and real users have been impacted by this on-chain. Here, a Uniswap user on Arbitrum tried to swap an exact amount 13.85 ETH (the native asset) for another token RDNT. This is an exact input swap: https://arbiscan.io/tx/0xd48a7427a059c053ea4cac9df2bb6ba3a1daa8d2dc5fc542c4c8b5bbc4272b29 Due to inadequate liqudity in the pool, they were only able to swap ~2.537 ETH. Since this is an exact input swap, `refundETH()` was never called, and the remaining balance was left in the router contract. A few transactions later, another unrelated unsuspecting user performing an exact output swap for an entirely different asset receives the ~11.3 ETH intended for the initial user: https://arbiscan.io/tx/0x210f85f4f70e84cc6bc0e8c084b91fe18c0e01042b7c507ff0f196391e78448c This happens one more time with this transaction leaving ETH in the router contract: https://arbiscan.io/tx/0x25a43f008a09c6a790dfcd6212f9e6c6044651fd68fd865348dd97a92576a37a And another unrelated user inadvertently receiving the ~11.7 ETH a couple minutes later: https://arbiscan.io/tx/0xddc3632a2e7329add851e34af6a6694f75dc7815ed6d74f9552be884c03ac37d What's interesting is both addresses that claimed the leftover ETH appear to be random EOA addresses and are probably just regular users, not bots. If this is a frequent occurrence, this is also an opportunity for MEV bots to sweep any ETH left in router contracts due to this bug. ## Uniswap Labs response Uniswap Labs was notified of this bug and has thus far refused to fix it. Their response to the Uniswap user referenced above is included below: > Sara Manjee (Uniswap Labs) Jul 28, 2022, 19:14 EDT Hi ***,   Thank you for your patience as we further investigated the issue you reported.   Our engineering team has reviewed the details of the swap you reported and has found the following info: • This swap was executed as an exactIn ETH→RDNT swap on an illiquid pool. This resulted in a trade with large price impact because Uniswap swapped to the max tick. • The quote was shown correctly in the interface. • The price impact confirmation screen was skipped because this swap was done in Expert Mode. • Your wallet received what was quoted in the interface. • Since this swap was on an illiquid pool, V3 did not take all the ETH in that was provided. Only enough ETH to swap to the max tick (approximately 2.5 ETH) was taken into the pool. The rest of the ETH was left in the router, which for “exact in” swaps is done by design for gas efficiency. This is not a bug. This was a low liquidity token, which comes with several risks that can result in undesired swap outcomes. When it comes to swapping assets that have low liquidity, low market cap or are brand new tokens, we encourage users to do their own research on price impact and keep a close eye on the details.   If you see something suspicious or have questions about swapping tokens, please reach out to support@uniswap.org for help.   For official information about the protocol and to get updates from the community, join us in Discord: https://discord.com/invite/FCfyBSbCU5   Stay Safe! Sara Clearly, Uniswap acknowledges the existence of the bug but refuses to recognize it as a bug and claims it is "by design for gas efficiency". How is the loss of a user's funds in specific corner cases such as this with zero warning in the UI that this may happen not a bug? If a user is losing funds with **zero** warning of the possibility, it is absolutely a bug. This could be fixed by either always including a call to `refundETH()` or at least including it in cases where the UI recognizes a pool as having inadequate liquidity. Until this is fixed, it would be wise to avoid exact input swaps in Uniswap pools with low liquidity. You can follow me on Twitter at [@0x534154](https://twitter.com/0x534154). Stay Safe!