Uniswap Vulnerability Disclosure

Reentrancy vulnerability in Uniswap's Universal Router

4/1/2023 - vdrg

Introduction

As part of our ongoing mission to contribute to the security and stability of the web3 ecosystem, at the end of last year our team discovered a potential vulnerability within Uniswap's Universal Router in the context of a bug bounty contest. In this blog post, we'll share our findings and insights into the issue.

The Vulnerability

Our analysis unraveled a scenario in which an attacker could reenter the router during a swap and drain a user's approved token balance. Picture this: the attacker creates a new token, a user tries to buy it using USDC in case it skyrockets in value, and suddenly, all their approved USDC balance is drained 🤯. The vulnerability affects the router's V3_SWAP_EXACT_OUT command, which is used to perform swaps with a fixed output amount through Uniswap V3 pools.

In decentralized exchanges, it's common for users to specify a "maximum amount" of tokens to be used for a swap. This feature allows users to set an upper limit on the tokens they're willing to spend, ensuring that if the token's price changes right before the swap execution, the exchange won't take more tokens than the user initially intended. The v3SwapExactOutput function, used for the V3_SWAP_EXACT_OUT command, is defined as follows:

function v3SwapExactOutput(
    address recipient,
    uint256 amountOut,
    uint256 amountInMaximum,
    bytes memory path,
    address payer
) internal {
    maxAmountInCached = amountInMaximum;

    // { perform the swap }

    maxAmountInCached = DEFAULT_MAX_AMOUNT_IN;
}

As it can be seen here, the router caches the amountInMaximum value inside the maxAmountInCached variable, which is then used to check if the amount of tokens being taken from the user is within the specified limit. The cached value will then be read at the end of the swap, when the router's uniswapV3SwapCallback function is called by the Uniswap pool to request the necessary funds. The uniswapV3SwapCallback will perform a check before using the payOrPermit2Transfer function to transfer tokens from the user to the pool:

function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes calldata data
) external {
    // ... omitted for brevity

    if (amountToPay > maxAmountInCached) {
        revert V3TooMuchRequested();
    }

    payOrPermit2Transfer(
        tokenOut,
        payer,
        msg.sender,
        amountToPay
    );

    // ...
}

What if we could manipulate both amountToPay and maxAmountInCached, in order to bypass the check and drain the user's approved balance? That's exactly what we can do:

  • To manipulate amountToPay, we just need to manipulate the token's spot price, which can be done by manipulating the pool's reserves. This is particularly easy if the attacker controls the token being bought. Otherwise, the attacker could just buy the token from the pool to increase its price, which isn't too hard for tokens with low liquidity.
  • To manipulate maxAmountInCached, we need to reenter the router during the swap operation. This is where the reentrancy mentioned earlier comes in. The attacker can reenter the system performing an arbitrary V3_SWAP_EXACT_OUT, which will overwrite the maxAmountInCached value with DEFAUT_MAX_AMOUNT_IN at the end of the v3SwapExactOutput function.

When the original swap execution continues, amountToPay will be huge, but maxAmountInCached will be even larger! So the check won't fail, and the attacker will be able to drain the user's approved balance ☠️.

It is important to note that token X doesn't need to be fully controlled by the attacker or be a malicious token, there only needs to be a way for the attacker to receive a callback when a transfer is being performed and for the attacker to be able to significantly manipulate X's spot price.

A simplified PoC that demonstrates the attack can be found here.

Reporting the Vulnerability

We informally communicated the vulnerability to a Uniswap employee on November 28, with the intention of expediting the process as the contest neared its conclusion and, as we were informed, the contracts were soon to be deployed. However, unbeknownst to us, the Uniswap team had rolled out an update a few hours prior, addressing a separate issue (presumably the one uncovered by the Dedaub team), which unintentionally invalidated our finding as it prevented reentering the router.

Although the contest was still ongoing and we believed our discovered issue to have a higher impact than the one previously reported, the Uniswap team recently informed us that they do not consider our issue to be critical. Their team's general consensus was that the reward should be granted to the first individual who reported the issue, as they deemed it to be the fairest approach, despite the issues not being entirely related.

Conclusion

We take pride in our contribution to the ongoing security and stability of the DeFi ecosystem, as the issue we discovered could have led to significant user losses had another issue not been detected prior to deployment. We extend our gratitude to the Uniswap team for their unwavering commitment to maintaining a secure platform and fostering open collaboration with the community. By sharing our experience, we aim to underscore the significance of timely audits, proactive security measures, and collaboration within the security community in ensuring the safety and success of this rapidly evolving landscape. Together, let's continue to break things and protect the users of these technologies 🏴‍☠️.

Thank you for reading!