Preface

With the rapid development of decentralized finance (DeFi), Uniswap, as a leading decentralized exchange, has been at the forefront of innovation. This article will deeply analyze the core mechanism of the Uniswap v3 protocol and explain its functional design in detail, including key functions such as centralized liquidity, multiple rates, token exchange and flash loans, and provide relevant audit points for auditors. (Note: The pictures in this article can be viewed in high definition at https://www.figma.com/board/QyIpAUR93MxZ4XZZf2QjDk/uniswap-v3.)

Architecture Analysis

The Uniswap v3 protocol mainly consists of four modules:

  • PositionManager: The main interface for users to perform liquidity operations. Users can use it to create token pools, provide/remove liquidity, and use ERC721 as a token for liquidity providers (LPs).

  • SwapRouter: The entry point for users to exchange tokens. Users can complete the token exchange operation through this module.

  • Pool: Responsible for token trading, liquidity management, collecting transaction fees, and Oracle data management. The Tick mechanism divides the price range into multiple fine scales.

  • Factory:用于创建和管理 Pool 合约。

流程梳理

Creating a Token Pair

用户可以通过 createAndInitializePoolIfNecessary 函数来完成。用户需传入代币对的 token0、token1、手续费(fee) 以及初始价格()。首先,系统会通过 getPool 函数检查该代币对是否已存在,如果尚未创建,则调用 createPool,并使用 CREATE2 指令进行交易对的部署。最后,通过 initialize 函数完成价格、手续费、tick、预言机等相关参数的初始化。

Providing liquidity

用户可以通过 mint 函数创建新的流动性头寸并生成对应的 NFT,或通过 increaseLiquidity 函数为现有的 NFT 流动性头寸增加流动性。首先,系统会检查交易是否在规定的时间范围内执行,然后调用 addLiquidity 函数完成具体操作。在该函数中,首先计算出池子的地址和流动性的大小,接着调用 updatePosition 更新用户的 Position,修改 lower、upper tick 以及累计的手续费总额。随后,系统通过 modifyPosition 添加流动性,确保 tick 满足上下限条件,返回计算出的 token0 和 token1 数量(int256),并将其发送到池中。最后,系统根据用户的 tokenId 更新对应的 Position 信息。

移除流动性

用户可以通过 decreaseLiquidity 函数来移除流动性。首先,系统会检查 LP 凭证的权限以及交易的时间有效性。在确保池子拥有足够流动性的前提下,调用 burn 函数来移除流动性。随后,系统会核实实际移除的代币数量是否满足用户设定的最小限度要求,并相应地更新用户的 Position 信息。

swap

用户可以通过 exactInput 函数指定支付的 token 数量以及期望获得的最小 token 数量,或通过 exactOutput 函数指定支付的最大 token 数量并设定期望获得的 token 数量。系统首先解析路径(path),然后依次调用 exactInputInternal 或 exactOutputInternal 函数完成每一步的 swap 操作。

在 swap 函数中,系统首先锁定 unlocked 状态,防止其他交易干扰状态变量的更新。进入循环后,系统通过 tick 找到下一个交易价格,并调用 computeSwapStep 函数计算每一步的交换,直到 tokenIn 或 tokenOut 达到用户预期。同时,系统会更新手续费、流动性、tick 以及价格的相关值。如果 tick 发生变化,还需要更新 Oracle 数据。完成这些操作后,系统将 tokenOut 支付给用户,用户再通过回调函数 uniswapV3SwapCallback 支付 tokenIn,这种机制可以被视为一种闪电交换(flash swap)。随后,系统会检查合约余额是否匹配,并在确认无误后解锁 unlocked 状态。

The transaction ends successfully when all swap operations in the path are completed and the transaction meets the user's expectations.

flash

Users can perform flash loan operations through the flash function. First, the system calculates the loan fee, and then sends the token required by the user to the specified loan address. Next, the system calls back the uniswapV3FlashCallback function implemented by the user, and the user completes the repayment operation in this function. The system checks the change in the contract balance after the callback to ensure that it matches the amount of the user's loan, and updates the corresponding fee. In addition to the flash function, users can also implement similar flash loan functions through swap operations, that is, borrowing and then repaying tokens during the transaction.

审计要点

1. Check whether refundETH is called after the swap operation

In the exactInput function, the user needs to specify the amount of tokens to pay and the minimum amount of tokens expected to be received. Before calling uniswapV3SwapCallback, the system will recalculate amount0 and amount1 to ensure that the user can send tokens accurately. However, when using ETH for swapping, the user needs to send ETH along with the transaction. Even if not all ETH is used during the transaction, the function will not automatically return the excess. The exactInput function only returns amountOut, so the trader cannot directly know how much ETH was actually consumed in this exchange.

此外,任何人都可以调用 refundETH 函数,从合约中提取未使用的 ETH。因此,建议检查 swap 操作后是否调用 refundETH 以防止用户未使用的 ETH 遗留在协议中,或使用 MultiCall 函数在一次操作中完成多个函数的调用。

2. Check if TWAP is implemented to get the oracle price

When Uniswap is used as a price source, there may be a risk of price manipulation when external protocols directly access Slot0 to obtain sqrtPriceX96. Attackers can manipulate the state of the liquidity pool through swaps and other means to obtain favorable prices when executing transactions.

为了降低这种风险,建议开发者进一步实现时间加权平均价格(TWAP) 来获取价格,因为 TWAP 能有效减少短期内价格的剧烈波动影响,使操纵价格的难度增加。

3. It is recommended to allow users to set their own slippage parameters

When other protocols use Uniswap v3 for swap operations, it is recommended that developers set slippage protection according to business scenarios and allow users to adjust parameters by themselves to prevent sandwich attacks. In this swap function, the fourth parameter sqrtPriceLimitX96 is used to specify the minimum or maximum price that the user is willing to perform the swap. This parameter can effectively prevent extreme price fluctuations during the transaction, thereby reducing the user's losses due to excessive slippage.

4. 建议引入流动性池白名单机制

在 Uniswap v3 中,基于不同的手续费(fee),同一对 ERC20 代币可能同时存在多个流动性池(Pool)。通常,少数流动性池拥有绝大部分的流动性,而其他池的总锁仓量(TVL) 可能非常少,甚至尚未创建。这些 TVL 较低的池更容易成为价格操纵的目标。

Therefore, when project parties choose to use liquidity pool data, they should avoid simply using LP as the data source. To ensure the reliability of data, it is recommended to introduce a whitelist mechanism to screen out pools with sufficient liquidity and difficulty in manipulation. This mechanism significantly reduces risk, ensuring the security and accuracy of price reference data, while preventing potential losses from manipulation of pools with too low TVL.

5. 检查是否在 TickMath.sol、FullMath.sol 和 Position.sol 中使用 unchecked

TickMath、FullMath 和 Position 等模块在 Uniswap v3 中用于执行复杂的数学计算,这些计算依赖于 Solidity 中的溢出处理机制。在早期的 Solidity 版本(<0.8.0)中,整数溢出和下溢行为默认不抛出异常,因此代码可以基于这种假设进行正常运行。然而,自 Solidity 0.8.0 版本开始,溢出和下溢会自动抛出异常,这会影响现有代码的执行。为确保这些模块在 Solidity 0.8.0 及更高版本中正常运行,开发者需要在特定函数中使用 unchecked 代码块,手动禁用溢出检查。这可以恢复之前版本中的行为,并确保高效执行溢出敏感的运算。

官方已经针对 Solidity 0.8.0 及更高版本做了相应的支持和调整,详情可参见此更新(https://github.com/Uniswap/v3-core/commit/6562c52e8f75f0c10f9deaf44861847585fc8129)。这一改动确保在新版编译器下,TickMath、FullMath 和其他相关模块能够继续正确运行。

6. 检查 path 编码解码方式是否相同

在 Uniswap v3 的 exactInput 和 exactOutput 函数中,用户需要输入 path 参数,该路径必须按照固定格式进行编码和解码,即 tokenA-fee-tokenB,用于逐步进行代币交换操作。这个路径结构明确指定了每一跳交易中涉及的两个代币以及它们之间的手续费级别。如果外部协议在使用 Uniswap v3 的代币交换功能时选择了不同的路径解码方式,可能会导致与 Uniswap 预期的路径格式不符。这种情况下,协议可能无法正确解析路径,从而无法成功执行预期的代币交换操作。

Therefore, developers are advised to ensure that external protocols strictly follow Uniswap's path encoding rules when integrating Uniswap v3's token swap functionality. To prevent path decoding errors, external protocols should carefully check the format of the path parameter when calling exactInput and exactOutput to avoid transaction failures or unexpected results.

7. 检查代币顺序是否影响项目逻辑

在 Uniswap 中,token0 是排序顺序较低的代币,用作基础代币(base token),而 token1 是排序顺序较高的代币,用作报价代币(quote token)。Uniswap 会根据两个代币的地址按字典序进行排序,确保代币对的顺序在池子中始终保持一致。

However, since the contract addresses of the same token on different blockchain networks may be different, especially for contracts deployed across chains, the order of tokens may change. This change can cause the roles of token0 and token1 to swap, affecting price performance. For example, on some chains, a particular token may be token0, but on other chains, it may be sorted as token1, resulting in a different relationship between the base token and the quote token, which ultimately affects the displayed price. Therefore, it is recommended that developers check whether the order of tokens will affect the project logic, especially in a cross-chain environment, and be sure to consider the price issues that may be caused by the order of tokens to avoid adverse effects on price performance and trading logic.

Summarize

上述基础检查项基于 Uniswap v3 当前版本,供审计人员对与 Uniswap v3 有交互的项目进行检查。不同项目的实现各具特点,因此审计人员需深入理解协议,并根据实际情况进行严格检查。对于正在开发的项目,慢雾安全团队建议开发者在开发过程中认真考虑这些检查项,以确保协议的安全性和可靠性。

作者 | Sissice

编辑 | Liz