深入剖析 UniSwap V3 原理及源码

一. 概述

Uniswap V3 是 Uniswap 协议的第三个版本,进一步优化了流动性提供者 (LP) 和交易者的体验。与前一版本相比,Uniswap V3 引入了一些重要的新特性和改进,主要包括集中流动性 (Concentrated Liquidity)主动市场管理,让流动性提供更加灵活和高效。

下面是一些 Uniswap V3 的核心要素和其优势:

1.集中流动性

在 Uniswap V2 中,流动性提供者 (LP) 必须在整个价格范围内(0 到 ∞)提供流动性。大多数资金实际上没有被有效利用,因为交易往往集中在特定的价格范围内。Uniswap V3 允许 LPs 在特定的价格区间内提供流动性,这称为“集中流动性”,提高了资金效率。这个功能可以让 LPs 将他们的资金集中在市场最活跃的价格区间,提升了收益率。

2.自定义价格范围

LPs 在 V3 中可以为他们的流动性设定特定的价格区间。例如,一个 LP 可以选择只在 1000 到 2000 美元之间的 ETH/USD 价格范围提供流动性。如果 ETH 价格超出这个范围,他们的流动性就不会被用于交易,这有助于降低无常损失(Impermanent Loss)。

3.多级费用结构

Uniswap V3 引入了多种不同的费用层级 (0.05%、0.3%、1%),供 LPs 选择,以适应不同的市场条件和风险偏好。交易对的波动性越大,LPs 通常会选择更高的费用层,以补偿更大的无常损失风险;对于稳定币对,则可以选择较低的费用层以吸引更多的交易量。

4.无常损失管理

集中流动性减少了资金在非活跃价格区间的分散,这有助于降低无常损失的风险。然而,LPs 仍然需要密切关注市场变化,主动调整他们的流动性价格范围。Uniswap V3 的这种设计促使 LPs 更加关注市场动向,进行主动管理。

5.NFT 表示流动性头寸

在 Uniswap V2 中,LP 头寸通过 ERC-20 代币表示,每个流动性池都使用相同的 LP 代币。然而在 V3 中,由于 LPs 可以设置不同的价格范围,流动性头寸是独特的,因此使用不可替代代币 (NFT) 来表示每个 LP 的头寸。每个 NFT 都包含了流动性提供者的自定义价格区间和资金量等信息。

6.自动路由

Uniswap V3 的交易功能会自动寻找最佳的交易路径,通过不同的交易对来最大化交易效率。例如,当你想要在 ETH 和 USDC 之间进行交易时,Uniswap 可能会通过多个中间交易对来找到最具成本效益的路径,减少滑点并节省交易成本。

7.资本效率提升

集中流动性让 LPs 在更窄的价格范围内提供流动性,极大地提高了资金的利用效率。在这些区间内,LPs 的资本效率比 Uniswap V2 提高了 4000 倍,意味着 LPs 可以用更少的资本获得与 V2 相同的收益。

8.主动管理策略

流动性提供者可以根据市场价格的变动主动调整他们的价格区间,从而最大化收益。尽管这需要更高的参与度和市场分析,但它为专业流动性提供者提供了新的套利和管理机会。

二. Uniswap v3 源码解读

Uniswap V3 的源码相比前几代进行了大量优化和改进,尤其是在流动性管理和协议的核心部分。其核心智能合约的设计十分模块化,包含多个独立的合约,这些合约相互配合实现了 Uniswap V3 的高效、灵活功能。我们可以从以下几个关键合约模块对 Uniswap V3 源码进行解析:

1.核心合约架构

Uniswap V3 的核心代码主要分为以下几大模块:

  • UniswapV3Factory.sol:负责创建和管理流动性池。
  • UniswapV3Pool.sol:每个流动性池的核心合约,管理流动性、手续费和交易逻辑。
  • Tick.sol 和 TickBitmap.sol:用于管理价格区间和流动性分布。
  • Oracle.sol:提供时间加权平均价格 (TWAP) 预言机功能。

2.UniswapV3Factory.sol

该合约是 Uniswap V3 中的流动性池工厂,用于创建新的流动性池。每一个交易对 (token0, token1) 都对应一个唯一的流动性池,这些池由工厂合约创建并管理。

关键方法

  • createPool(address tokenA, address tokenB, uint24 fee):创建一个新的流动性池,tokenA 和 tokenB 是两个交易代币的地址,fee 是交易的手续费率(0.05%、0.3%、1%)。

事件

  • PoolCreated(address token0, address token1, uint24 fee, address pool):每当一个新池子被创建时,都会触发此事件,包含了池子的相关信息。

实现逻辑

  • 工厂合约管理了池子地址的映射表 (token0, token1, fee) => pool address,以确保每个独立交易对的池子只会被创建一次。

3.UniswapV3Pool.sol

这是 Uniswap V3 最核心的合约之一,每一个池子都会部署一个 UniswapV3Pool 合约实例。该合约管理流动性、交易、价格区间和手续费等。

关键功能

  • 交易和流动性管理
    💡swap(...):进行代币交换的函数。它根据池子的当前状态和提供的流动性来进行交换,同时根据价格滑动(slippage)调整交易价格。
    💡mint(...):添加流动性的函数,允许用户在指定的价格区间内存入资金并获取相应的流动性份额。
    💡burn(...):撤回流动性,用户可以根据自己提供流动性时指定的价格区间提取资金。
  • 价格区间管理
    💡tickSpacing:每个流动性池都划分为多个价格区间(Ticks),该变量定义了每个 Tick 之间的距离。Uniswap V3 通过这种方式实现集中流动性。
  • 价格预言机功能
    💡observe(...):获取指定过去时间段的价格,以便计算 TWAP(时间加权平均价格)。这个功能对许多 DeFi 应用非常重要。

状态变量

  • slot0:存储了池子的核心状态,包括当前价格、Tick、流动性等信息。
  • liquidity:当前价格范围内的总流动性。

4.Tick 和 TickBitmap

Uniswap V3 的一个重大创新是价格区间的管理。Tick.sol 和 TickBitmap.sol 负责管理池子的流动性分布和价格区间,帮助池子通过集中流动性提高资本效率。

  • Tick.sol:这个合约管理具体价格区间(Ticks)中的流动性。流动性提供者可以在指定的 Tick 范围内提供流动性,而池子需要通过 Tick 来更新流动性和价格状态。
  • TickBitmap.sol:由于价格区间非常多,为了节省计算和存储空间,Uniswap V3 使用了位图来标记有效的 Tick 位置。TickBitmap.sol 合约通过位图的方式跟踪哪些价格区间有流动性,以加速价格更新和交易执行。

5.Oracle

Oracle.sol 是 Uniswap V3 提供的预言机模块。它允许用户访问 Uniswap 池子的时间加权平均价格 (TWAP),为其他合约提供价格参考。这个合约基于历史价格和交易数据计算出平均价格,并可以防止短时间内的价格操纵。

  • 关键函数: 💡observe(...):返回一系列过去时间点的价格数据。 💡snapshotCumulativesInside(...):根据给定的 Tick 区间,返回累计价格和流动性信息。

6.流动性提供和管理

Uniswap V3 对流动性提供者引入了集中流动性的设计,允许 LPs 在指定的价格范围内提供流动性。这是通过 mint(...) 和 burn(...) 函数实现的。

mint() 流程

  • LP 指定价格区间,并提供对应的代币。
  • 系统根据 LP 的输入计算流动性,并更新相应价格区间的状态。
  • LP 获得相应的流动性份额表示,并通过 NFT 表示其头寸。

burn() 流程

  • LP 指定从哪个价格区间撤回流动性。
  • 系统根据当前价格和流动性状态,计算可以提取的代币数量。
  • 返回 LP 的代币并销毁其流动性份额。
  • 手续费模型

Uniswap V3 支持多种手续费模型,LPs 可以根据不同的市场状况选择不同的手续费率(0.05%、0.3%、1%)。这些手续费会积累在流动性池中,并且可以通过 collect(...) 函数提取。

三. Uniswap v3 详解代码流程

1.创建并初始化流动性池

上面流程图中的代码都比较简单,难一点的就是 getTickAtSqrtRatio,getTickAtSqrtRatio 里面的代码解析

该函数通过一系列位运算和对数计算,高效地在给定的平方根价格下找到相应的 Tick 值。Tick 是 Uniswap V3 中管理价格区间的关键,通过这个函数可以快速确定给定价格在哪个 Tick 区间内,从而实现高效的流动性管理和交易撮合。这种计算方式采用了位操作和内联汇编,确保了在区块链上的执行效率。

  • 检查价格范围
require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R');

函数首先通过 require 检查输入的 sqrtPriceX96 是否在允许的最小和最大范围之间,防止出现价格超出合约预期的异常情况。

  • 计算比率
uint256 ratio = uint256(sqrtPriceX96) << 32;

将 sqrtPriceX96 左移 32 位,以增加计算精度。这个比率将用于之后的二进制搜索和对数计算。

  • 寻找最高有效位(msb)

通过多个内联的 assembly 块,函数执行了一系列位移操作,逐步确定比率 ratio 的最高有效位(most significant bit,msb)。这种方式用于加速找到 ratio 在二进制中的位置,类似于高效的对数计算。

assembly {
    let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
    msb := or(msb, f)
    r := shr(f, r)
}
// ... (类似的逻辑)

这段代码依次比较 r 的大小,找出 r 的最高有效位位置,并记录在 msb 中。

  • 计算二进制对数 log_2

通过对 ratio 的最高有效位进行处理,计算出以 2 为底的对数值 log_2。

int256 log_2 = (int256(msb) - 128) << 64;

这里的逻辑是将最高有效位 msb 调整为固定的范围,并生成一个初始的 log_2 值。

  • 逐步提高对数精度

函数继续对 r 进行一系列平方和位移操作,逐步将 log_2 的精度提高,确保其精度达到 128 位,最终用于更精确的 Tick 计算。

  • 计算 log_sqrt10001
int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number

这里计算了一个常数乘以 log_2,生成一个以 128.128 格式存储的高精度值。这个数值是 Tick 和 sqrtPriceX96 之间关系的对数表示。

  • 计算 Tick 的上限和下限
int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128);
int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128);

通过对 log_sqrt10001 进行位移和加减操作,分别计算出可能的最大 Tick 值 (tickHi) 和最小 Tick 值 (tickLow)。

  • 确定最终的 Tick 值
tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow;

最后,根据计算出的 tickLow 和 tickHi 的值,确定最终的 Tick。函数会选择一个保证 getRatioAtTick(tick) 小于等于 sqrtPriceX96 的最大 Tick 值。

2.添加流动性

  • MintParams 参数解释:
    💡token0: 组成池子的token0
    💡token1:组成池子的token1
    💡fee:费率
    💡tickLower: 价格区间的下限对应的 tick 序号
    💡tickUpper: 价格区间的上限对应的 tick 序号
    💡amountoDesired: 要添加作为流动性的token0数量
    💡amount1Desired: 要添加作为流动性的token1数量
    💡amountoMin: 作为流动性的token0最小数量
    💡amount1Min: 作为流动性的tokne1最小数量
    💡recipient: 接收头寸的地址
    💡deadline: 过期时间

其中,amountODesired 和amountlDesired1 其实是预估的数量。从用户端发起交易,到实际链上执行交易是存在时延的,这期间可能有其他用户也添加了流动性,所以最终成交时的数量可能会和 Desired 的值不一样。如果期间价格变化比较大,也会导致用户实际成交时的滑点很大,因此在前端页面上其实会有一个滑点设置来保护用户实际成交时不会超 amountoMin 和 amount1Min 就是根据设置的滑点值计算出来的。

1.1.Pool Mint 函数

function mint(
    address owner,
    int24 tickLower,
    int24 tickUpper,
    uint128 amount,
    bytes calldata data
) external returns (uint256 amount0, uint256 amount1)
  • owner: 提供流动性者的地址,这个地址会被记录为流动性拥有者。
  • tickLower: 流动性提供的下边界(Tick),决定了在哪个价格区间开始提供流动性。
  • tickUpper: 流动性提供的上边界(Tick),决定了在哪个价格区间结束流动性。
  • amount: 提供的流动性数量(liquidity)。
  • data: 回调数据,在跨合约调用时使用。

返回值

  • amount0: 实际为 token0 提供的数量。
  • amount1: 实际为 token1 提供的数量。

1.1.1 mint 函数的核心流程

在 Uniswap V3 中,mint 函数执行以下几个关键步骤来完成流动性添加。

输入验证

require(tickLower < tickUpper, 'TLU');  // 确保下边界比上边界低
require(tickLower >= TickMath.MIN_TICK && tickUpper <= TickMath.MAX_TICK, 'TLM');
  • Tick 验证: 合约首先会检查 tickLower 是否小于 tickUpper,并确保它们都在允许的 MIN_TICK 和 MAX_TICK 范围内。
  • 下边界与上边界: tickLower 和 tickUpper 代表了 LP 提供流动性的价格区间。

计算流动性所需的代币数量

合约根据 tickLower 和 tickUpper 的价格区间、池子当前的价格和流动性情况,计算出所需的代币数量。

(amount0, amount1) = _modifyPosition(
    owner,
    tickLower,
    tickUpper,
    int256(amount)
);
  • 调用 _modifyPosition: 这是 mint 函数的核心,它处理流动性的位置和代币计算。_modifyPosition 根据流动性的位置调整相应的池子状态(包括流动性、代币余额等),并返回实际提供的 token0 和 token1 的数量。

内部方法 _modifyPosition

_modifyPosition 函数通过以下步骤计算 LP 需要提供的代币数量,并更新池子的状态:

  • 计算代币数量: 根据流动性提供的价格区间和池子当前的价格,_modifyPosition 函数会根据 Uniswap V3 的定价机制,计算出需要的 token0 和 token1 的数量。其核心计算依据 getAmount0ForLiquidity 和 getAmount1ForLiquidity。
  • 流动性变化: 当流动性添加到某个价格区间时,函数会根据 tickLower 和 tickUpper 调整流动性位图,并增加该区间内的流动性。
  • 更新池子状态: 池子会根据新添加的流动性重新计算其价格状态、代币余额和手续费。

代币转移

接下来,合约会从流动性提供者的地址中转移所需的代币(token0 和 token1),并将这些代币注入到流动性池中。

if (amount0 > 0) {
    _transferFrom(token0, owner, address(this), amount0);
}
if (amount1 > 0) {
    _transferFrom(token1, owner, address(this), amount1);
}
  • 代币转移: 合约根据计算出的 amount0 和 amount1,将代币从 owner 地址转移到合约中。
  • 合约调用 _transferFrom: 这个内部函数执行实际的代币转账操作,将流动性提供者的 token0 和 token1 转移到流动性池合约中。

流动性位置记录

在完成流动性添加后,Uniswap V3 会通过 NFT(不可替代代币)记录流动性提供者的头寸(即在什么价格区间内添加了多少流动性)。这个 NFT 包含 tickLower、tickUpper 和 liquidity 信息,用来代表流动性提供者的头寸。

流动性提供计算的核心逻辑

在 Uniswap V3 中,流动性提供的核心在于如何根据价格区间(tickLower 和 tickUpper)计算 token0 和 token1 的数量。池子当前的价格和流动性影响着用户需要提供多少代币。

计算 token0 和 token1 数量

通过以下公式计算提供流动性时需要的 token0 和 token1 数量:

  • token0 计算公式:

$$amount0=liquidity×(\sqrt {priceUpper}−\sqrt {priceLower})\over \sqrt {currentPrice}$$

  • token1 计算公式:

$$amount1=liquidity×(\sqrt {currentPrice}−\sqrt {priceLower})$$

这两个公式基于 Uniswap 的定价机制,流动性在价格区间内的分布决定了所需的代币数量。

流动性与价格关系

  • tickLower 和 tickUpper 决定了流动性的价格区间,流动性只能在这个区间内被使用。
  • 当价格在区间下限(tickLower)附近时,更多的流动性会以 token0 的形式存在;当价格接近上限(tickUpper)时,更多的流动性会以 token1 的形式存在。

手续费处理

在 Uniswap V3 中,流动性提供者除了获得交易对之间的价格变化带来的收益外,还能获得交易过程中产生的手续费。手续费的累积与流动性提供的价格区间和所提供的流动性成正比。

  • 当价格位于流动性提供者指定的价格区间时,每一笔交易都会按照手续费率(可能为 0.05%、0.3% 或 1%)收取手续费,并累积到流动性提供者的账户中。
  • mint 函数会记录这个流动性提供的区间和数量,以便后续交易时分配手续费。

mint 流程总结

  • 验证输入参数:首先验证 tickLower 和 tickUpper 是否有效,确保价格区间合法。
  • 计算代币数量:通过 _modifyPosition 函数计算在指定价格区间内需要提供的 token0 和 token1 的数量。
  • 代币转移:将计算出的 token0 和 token1 从流动性提供者转移到流动性池合约中。
  • 更新流动性位置:通过 NFT 记录流动性提供者的头寸,确保流动性和手续费可以正确分配。

3.增加流动性

4.减少流动性

函数入参除了指定头寸的 tokenld,还指定了liquidity,这是要移除的流动性数量。

实现逻辑倒也简单,先通过 tokenld 从_positions 读取出 Position 对象,然后校验头寸里的流动性不能小于要移除的流动性。之后,计算出 pool 地址,并调用 pool 合约底层的burn 函数来实现底层的移除流动性操作。然后,和增加流动性时一样,结算之前的手续费收益并更新手续费相关字段,移除的流动性也相应从头寸中减少。

5.提取手续费收益

6.销毁头寸

7.兑换

  • 其中:需要跨多个池子的编码方式如下:

7.1.swap 函数概述

swap 函数位于 UniswapV3Pool.sol 合约中,负责执行两个代币间的交换。其函数签名如下:

function swap(
    address recipient,
    bool zeroForOne,
    int256 amountSpecified,
    uint160 sqrtPriceLimitX96,
    bytes calldata data
) external returns (int256 amount0, int256 amount1);

参数解释

  • recipient: 交易结束后接收代币的一方。
  • zeroForOne: 确定交易方向的布尔值。如果为 true,则表示将 token0 换成 token1;如果为 false,则是将 token1 换成 token0。
  • amountSpecified: 交易的指定金额,正值表示用户愿意输入多少代币,负值表示用户希望输出多少代币。
  • sqrtPriceLimitX96: 限制交易价格不能超出该值(单位为价格平方根)。
  • data: 用于在跨合约调用时传递的数据,通常用于回调机制。

返回值

  • amount0: 交易过程中实际涉及的 token0 数量。
  • amount1: 交易过程中实际涉及的 token1 数量。

7.2.swap 函数的执行流程

Uniswap V3 的 swap 流程通过多步操作动态调整价格、跨越价格区间、处理流动性并计算交易手续费。下面是 swap 流程的详细步骤。

7.2.1 验证参数

函数开始时,合约会验证一些关键参数:

  • sqrtPriceLimitX96 作为价格限制,确保交易的价格不会超出设定范围(防止滑点超出预期)。
require(sqrtPriceLimitX96 > MIN_SQRT_RATIO && sqrtPriceLimitX96 < MAX_SQRT_RATIO, 'Invalid sqrt price limit');

这一步确保了交易的价格不会跌破最低价或超过最高价。

7.2.2 确定交易方向

根据 zeroForOne 的布尔值决定交易的方向:

  • zeroForOne == true:表示用户将 token0 换为 token1,价格会下降(price 从高向低移动)。
  • zeroForOne == false:表示用户将 token1 换为 token0,价格会上升(price 从低向高移动)。

在 Uniswap V3 中,价格是通过 sqrtPriceX96(价格的平方根)来表示的,因此 swap 过程中价格的变化通过 sqrtPriceX96 的移动来完成。

7.2.3 核心价格更新和流动性处理

在每次交易过程中,swap 函数会逐步调整池子的价格状态,并检查当前价格区间内是否有足够的流动性支持交易。

在当前价格区间执行交易: 在初始的价格区间中,合约会计算当前价格对应的流动性和代币数量,并进行交易。

(amount0, amount1) = _swapStep(...);
  • _swapStep 函数是 swap 流程的核心步骤,在这里进行价格的调整和代币的交换。函数会: 计算基于当前流动性和价格变化的代币交换比例。 调整价格 sqrtPriceX96 并在此价格下完成交换。
  • 价格跨越 Tick: 当价格跨越一个 Tick(即价格跨越一个区间边界),需要对流动性和价格进行更新。每个 Tick 定义了一个价格区间,当价格跨越该区间时,合约会检查下一个价格区间的流动性。
(amount0, amount1) = _crossTick(...);
  • crossTick 函数会更新池子的价格和流动性状态。跨越 Tick 意味着价格从一个区间移动到另一个区间,这需要重新计算价格和可用流动性。

7.2.4 滑点保护机制

用户在交易时可以通过 sqrtPriceLimitX96 来限制价格变动的范围,确保价格滑点不会超出用户预期。

  • 如果交易价格逼近 sqrtPriceLimitX96,合约会中止交易,防止滑点超过预期。

7.2.5 手续费计算

Uniswap V3 引入了多级手续费系统,根据流动性池的手续费层级(如 0.05%、0.3%、1% 等),交易过程中会累积手续费。

  • 在每个 Tick 区间内,手续费根据交易量按比例计算并累积。
  • 累积的手续费会记录到流动性池中,流动性提供者可以在合适的时候提取这些手续费。
(uint256 feeAmount0, uint256 feeAmount1) = _collectFees(...);

7.2.6 价格区间结束与交易结算

交易在达到指定的 sqrtPriceLimitX96 或 amountSpecified 被完全交换后结束。swap 函数返回实际的 amount0 和 amount1,表示最终交换的代币数量。

return (amount0, amount1);

7.3.swap 过程中的核心组件

Uniswap V3 的 swap 过程依赖于几个核心组件和数据结构:

7.3.1 Tick

  • Tick 是 Uniswap V3 中的价格区间,用来表示价格的离散状态。在流动性提供者设置的价格区间中,交易会跨越多个 Tick。每个 Tick 对应一个价格区间,并记录在该区间内可用的流动性。

7.3.2 流动性管理

Uniswap V3 中的流动性是集中管理的,流动性提供者可以选择在哪个价格区间内提供流动性。swap 过程中,当价格跨越一个 Tick 时,合约会检查是否在下一个 Tick 区间有足够的流动性,如果有则继续执行交易。

7.3.3 价格管理

价格在 Uniswap V3 中是通过 sqrtPriceX96(价格的平方根)来表示的。每次交易时,价格根据交易方向和交易量在不同的 Tick 区间内移动。sqrtPriceX96 的变化速度取决于池子的当前流动性和交易的代币数量。

7.3.4 手续费管理

Uniswap V3 支持多级手续费,流动性池的创建者可以选择合适的手续费层级(0.05%、0.3%、1%)。在交易过程中,合约会按比例收取手续费,累积在池子中。流动性提供者可以通过 collect 提取累积的手续费。

swap 的计算公式: 在 Uniswap V3 中,价格是基于流动性、代币数量和 Tick 值动态调整的。关键的计算公式如下:

  • 价格变化公式

$$sqrtPrice_{new}=sqrtPrice_{old}±{Δ\over liquidity}$$

  • sqrtPrice 表示价格的平方根。
  • Δ 表示交易中输入或输出的代币数量。
  • liquidity 表示当前区间内的流动性。
  • 手续费计算公式

$$fee=amount×feeRate$$

  • amount 为交易代币的数量。
  • feeRate 为手续费率(如 0.05%、0.3%、1%)。

四.总结

Uniswap V3 的特性使其成为一个高度灵活和高效的去中心化交易平台,它的主要创新点包括:

  • 集中流动性,提高了资金效率;
  • 多级手续费层,允许流动性提供者根据风险偏好选择不同的费用结构;
  • NFT 表示流动性头寸,为流动性提供者带来更多的灵活性;
  • 改进的预言机机制,为 DeFi 应用提供精确的价格数据;
  • 更灵活的价格滑点保护,使用户能够更安全地进行交易。

Uniswap V3 的这些创新让它在 DeFi 领域处于领先地位,适合各种类型的用户,无论是流动性提供者、交易者,还是构建在其上的应用开发者。

全部评论(0)