π¦ Uniswap Vulnerability Disclosure
Reentrancy vulnerability in Uniswap's Universal Router
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 arbitraryV3_SWAP_EXACT_OUT
, which will overwrite themaxAmountInCached
value withDEFAUT_MAX_AMOUNT_IN
at the end of thev3SwapExactOutput
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!