Uniswap V3

通过AMM进行代币交换时,预期价格和实际交易价格之间必然产生滑点(Slippage)。并且当池中的流动性不足且交易量大的时候,滑点会加大,滑点越大交易者就不得不以更高价格来交易代币。为了在一定程度解决滑点问题,出现了变化流动性区间——使得Uniswap V3成为设计最灵活、最高效的AMM(自动做市商)。流动性提供者(Liquidity Providers)可以根据需要自行定义该区间范围,设定这个范围意味着供给的代币仅能在“特定价格范围”内进行交换,并将资金集中在某个价格区间内,滑点更下,极大提高资本效率,提升了交易深度和收益率。

集中流动性

流动性供给限定在特定价格范围内被称为集中流动性(Concentrated Liquidity)。通过这种方式可以避免供给者承担未用于交换的流动性,从而实现更高效的流动性供给。流动池里面的资金在特定的价格范围内都将被利用。

准时流动性(JIT Liquidity)

准时流动性(JIT Liquidity) 是指流动性提供者(LP)在某一笔交易执行前瞬间加入流动性,然后立即在交易之后移除流动性,从而捕获费用收益的一种策略。

这是一种链上可编程交易的行为,通常借助 MEV(矿工可提取价值)策略 实现。

JIT 流动性利用这一点进行以下操作:

  1. 某个交易者(Taker)准备进行一笔大额交易(如从 ETH → USDC)。
  2. MEV 机器人或 LP 监测到这笔交易会导致价格进入某一特定范围。
  3. 在这笔交易被打包前,机器人向该价格区间注入精确的流动性(例如只在 ETH/USDC 的某个价格区间)。
  4. 交易发生,JIT 流动性被使用 → 机器人获得手续费
  5. 机器人立即移除流动性,并取回原始资产 + 所得手续费。
  6. 一般情况下,不承担资产波动风险。

JIT流动性的目的:

  • 捕获手续费
  • 实现“无风险套利”
  • 为大交易提供临时深度,提高成交效率

==潜在问题与争议==

对长期 LP 不公平
  • 长期 LP 承担价格波动和无常损失(impermanent loss)
  • JIT LP 只在精准时间加入并立即退出,“白嫖”手续费
引发治理和设计讨论
  • 是否应该对 LP 收益做时间加权?
  • 是否需要引入 “minimum lock time” 或 “delayed fee accrual”等机制防止 JIT 利用?
对用户交易有一定好处
  • 可以提高交易深度,减少滑点
  • 有时可帮助大交易者获得更优的成交价格

流动性净变动(Liquidity Net)

Liquidity Net(流动性净变动)表示:当价格发生变化时,它最终会跨越一个 tick,而在跨越这个 tick 时,会在当前流动性上加上或减去某个数值,这个数值就是所谓的 Liquidity Net。

未用于交换的流动性被称为懒惰流动性(Lazy Liquidity)

高效的流动性供给结果被称为高资本效率(Higher Capital Efficiency)

在V3,由于直接兑换的流动性不足,兑换可能被路由到多个交易对。路由的交易对越多,交易者将支付更多的费用,滑点也随之提高。

相对于V2的创新点:

  • 相对于Uniswap v2,LP可以提高4000倍的资本效率提供流动性,以获得更高的资本回报
  • 资本效率为低滑点的交易执行铺平了道路,可以超越中心化交易所和专注于稳定币的AMM
  • LP可以显著增加对优先资产的敞口,并降低其下行风险
  • LP可以通过在完全高于或低于市场价格的价格区间内增加流动性,类似于沿平滑曲线执行的收费限价单来卖出另一种资产。Uniswap V3只需一种代币也可以提供流动性,还可以构建价格范围限价单(Range Limit Orders)
  • V3预言机可以更简单和更低成本的集成。它可以根据需求提供过去约9天内任何时期的时间加权平均价格(TWAP)。这样集成项目就不需要检查历史价格值。

通过这些设计,在以太坊主网上使用V3兑换的Gas成本比V2略低。但是在Optimism部署上进行的交易将可能更大程度地便宜。

V2只有在贸易规模极大(x*y = K ,K 总流动性的恒定值很大)的时候,价格影响才不明显。而V3很好的解决了在没有高资本需求的情况下实现高流动性并且提高了资本效率。

所以引入了集中流动性,即LP可以根据预期价格在特定范围内调整汇率。旨在最大程度的减少汇率波动以及减少所需的储备量。如果市场价格在LP指定的价格区间之外变动,其流动性就会被有效地从资金池中移除,不在赚取费用。在这种状态下,LP的流动性完全由两种资产价值较低的一种组成,直到市场价格回到他们指定的价格区间,或者他们考虑到当前的价格,更新价格区间。再一个就是由于流动性可以都集中在攻击者的目标价格之间的区间,那么攻击者需要动用更多的资金才能达到相同的目的。从这个角度来看,攻击者进行经济攻击的难度增大了。当然,攻击者还是可能通过闪电贷等方式获得足够的资金来完成攻击。

LP只有市场在其指定的价格区间内交易才会赚取费用。

V2的缺点:

  • 首先就是,交易时会发生滑点导致未能以预期价格购入或卖出。除非流动池里面的资金规模极大,市场行为无法扰动流动池的平衡(理论上可行,实际上无法实现:LP投入高,获利少甚至无利可循,因为还有无偿损失,有很高的价格风险)。
  • 其次就是资金利用率不高,因为V2维持的汇率是从0到$\Large∞$,而市场上的交易价格始终会维持在一定区间内。在这个价格区间内,LP预期可以看到最多的交易量,从而赚取最多的费用。相比之下,V3维持这个价格区间所需要的资金要远少于V2。
  • 最后,恒定价格公式简单,市场获利窗口少,面对小股资金的流入和流出,吸引力小。

更高的流动性(Higher Liquidity) ⇒ 更小的价格影响(Less Price Impact) ⇒ 更多交易者(More Traders)

Uniswap V3 可以利用较少的资金构建出一个更“宽大的”恒定乘积曲线,使得流动性都集中作用在这个价格区间内。

在这个价格区间内实际用到的资金被称为真实储备(Real Reserves),即该价格区间内真正需要的代币数量。而为了构建相同的曲线,在 Uniswap V2 中需要的代币(远比Uniswap V3 所用的多的多),则被称为虚拟储备(Virtual Reserves),是理论上需要的数量。

$\Large(x_r + x_v)(y_r + y_v) = L^2$

$\Large(x_r + \frac{L} {\sqrt{P_b}}) (y_r + L {\sqrt{P_a}}) = L^2$

在Uniswap V2中,智能合约追踪的是代币X和代币Y的储备量。基于这些储备,我们可以计算出流动性和价格(汇率)。而在Uniswap V3中, 智能合约追踪的是流动性(L)和价格(P),通过这些数据,可以计算在该价格区间内(PA到PB)的X和Y的储备。

  • X 的公式为:

​ $\Large x_v = \frac{L^2}{L \cdot \sqrt{P_b}} = \frac{L}{\sqrt{P_b}}$

​ $\Large x = \frac{L}{\sqrt{P_a}} - \frac{L}{\sqrt{P_b}}$

  • Y的公式为:

    ​ $\Large y_v = \frac{L^2}{L / \sqrt{P_a}} = L \cdot \sqrt{P_a}$

    ​ $\Large y = L \cdot (\sqrt{P_b} - \sqrt{P_a})$

这两个公式可以通用。

  • 对于 token x 的计算,我们是考虑价格从低到高的变化;
  • 对于 token y 的计算,我们是考虑价格从高到低的变化。

已知流动性L和价格P,如何计算从当前价格P上升到某一目标价格Pb所需要的token x的数量:

$x = \Large\frac{L}{\sqrt{P}} - \Large\frac{L}{\sqrt{P_b}}$

如何计算从当前价格下降到某一目标价格Pa所需的token y的数量:

$y = L \cdot (\sqrt{P} - \sqrt{P_a})$

如果我们添加或移除一定数量的 token x,价格会变成多少?

  • 添加 $\delta x$ 后新的价格(平方根形式):

    ${\sqrt{Pa}} = \Large\frac {L \cdot {\sqrt{Pb}}} {L + \delta x \cdot {\sqrt{P~b}}}$

  • 移除 $\delta x$ 后新的价格(平方根形式):

    ${\sqrt{Pb}} = \Large\frac {L \cdot {\sqrt{Pa}}} {L - \delta x \cdot {\sqrt{P~a}}}$

如果我们添加或移除一定数量的 token y,价格会变成多少?

  • 添加 $\delta y$后新的价格(平方根形式):

    ${\sqrt{Pb}} = {\sqrt{Pa}} + \Large\frac{\delta y} {L} $

  • 移除 $\delta y$后新的价格(平方根形式):

    ${\sqrt{Pa}} = {\sqrt{Pb}} - \Large\frac{\delta y} {L}$

计算流动性:

  • 当前价格小于Pa,所有的流动性都表现为token x,因为当价格从Pa升高到Pb时,流动池必须释放出相应数量的token x

​ $L = \Large\frac{x} {\frac{1} {\sqrt{p_a}} - \frac{1} {\sqrt{p_b}}}$

或者

$L = \Large\frac{x \cdot {\sqrt{p_a}} \cdot{\sqrt{p_b}}} {{\sqrt{p_b}} - {\sqrt{p_a}}}$ $\delta L = \Large\frac{\delta x} {\frac{1} {\sqrt{p_a}} - \frac{1} {\sqrt{p_b}}}$
  • 当前价格大于Pb,所有的流动性都表现为token y,因为当价格从Pb下降到Pa时,token y必须从池中释放。

    $L = \Large\frac{y} {{\sqrt{p_b}} - {\sqrt{p_a}}}$ $\delta L = \Large\frac{\delta y} {{\sqrt{p_b}} - {\sqrt{p_a}}}$
  • 当前价格Pa<P<Pb,区间 [P, Pb] 的流动性部分以 token x 形式存在,而区间 [Pa, P] 的流动性部分以 token y 形式存在。这两部分的流动性不一定相等。

    Lx表示从当前价格 P 到 Pb 范围的流动性(即 token x 部分)

​ $L~x = \Large\frac{x} {\frac{1} {\sqrt{pa}} - \frac{1} {\sqrt{p_b}}}$$\delta L = \Large\frac{\delta y} {{\sqrt{p_b}} - {\sqrt{p_a}}}$

​ Ly表示从 Pa 到当前价格 P范围的流动性(即 token y 部分)

​ $L~y = \Large\frac{y} {{\sqrt{p}} - {\sqrt{p_a}}}$

​ $\delta L = \Large\frac{\delta y} {{\sqrt{p_b}} - {\sqrt{p_a}}}= \Large\frac{\delta x} {\frac{1} {\sqrt{p_a}} - \frac{1} {\sqrt{p_b}}}$

流动性管理方式的不同

  • V2 中,流动性管理是被动的,用户只需提供代币即可获得手续费收入。流动性通过 ERC20 代币表示。
  • V3 中,流动性管理是主动的。用户必须选择一个价格区间来添加流动性,只有在当前价格落在该区间内时,才能获得手续费。

如果价格偏离所设区间,流动性将不再产生费用。此时用户需要重新定位自己的流动性到新的价格区间内。由于每一份流动性的价格范围和提供时间都可能不同,因此无法再用 ERC20 来表示。V3 使用的是 ERC721(NFT) 来代表每一份独特的流动性头寸。两者的手续费结构也不同,V2仅支持一种固定手续费:0.3%,而V3支持四种不同的手续费档位:0.01%、0.05%、0.3%、1% 此外Uniswap 的治理机制可以新增更多费率档位。

TWAP(时间加权平均价格)的计算方式不同

  • V2 使用算术平均值(Arithmetic Mean)快速响应价格变化,但对极端价格波动较敏感

  • V3使用几何平均值(Geometric Mean)平滑了价格波动,但在价格突变响应有一定的滞后性。

Uniswap V3 的缺点:

  • 需要主动管理:一旦价格偏离设定区间,就无法继续赚取手续费,LP需要重新部署流动性。
  • 流动性为NFT(ERC721):这使得流动性难以转移或组合,也对智能合约继承带来更高的开发复杂度

区间的宽度对流动性的影响

  • 区间越窄(例如 0.999 ~ 1.001),单位资本提供的流动性越高,价格滑点越小;
  • 区间越宽(例如 0.5 ~ 1.5),流动性被摊薄,每个价格点的可用代币减少,滑点变大;
  • 如果区间扩大到从 0Uniswap V3 的流动性曲线会退化成 V2 的那条曲线

Uniswap V3中的现货价格公式:$\Large p = 1.0001^t$

在这个公式中:

  • p 表示当前的现货价格;

  • t 表示当前的 tick 值(整数); t 被称为 tick index(价格跳动指数),Tick index 是 范围[−887272,887272] 内的整数,从而在价格曲线上产生 1,774,545 个价格跳动,从 p(−887272) 到 p(887272)。

    • 为什么是887272呢?由$p = 1.0001^t$公式我们可以推得:$\log_{1.0001}(p) = \log_{1.0001}(1.0001^t) = t$,而p最大为2^128^,代入可得$t = \log_{1.0001}(2^{128}) = 887272$
  • 智能合约会跟踪当前的 tick(价格跳动),然后通过该公式计算价格。

Tick 与价格的关系:

  • tick = 0 时,p = 1,价格等于 1;
  • tick < 0(负数)时,p < 1,价格小于 1;
  • tick > 0(正数)时,p > 1,价格大于 1。

tick 实际上是一个价格的离散表示,tick 的每一个单位代表价格变动的一个微小步长。这个 1.0001^t^ 的形式,是为了在可计算性和精度之间取得平衡。并不是所有这 177 万个 tick 都能在池中使用,具体哪些 tick 可用取决于池创建时设置的 tick 间距(tick spacing)。每个池都定义了一个值叫 tickSpacing,它决定了允许的两个相邻 tick 索引之间的距离。当流动性提供者(LP)向 Uniswap V3 池中添加流动性时,需要指定一个价格区间并会设定好变量 tickSpacing 。而且,tick spacing 与手续费费率是一一对应的。。这个价格区间是通过一个下限 tick 和一个上限 tick 来定义的。然而,这些 tick 不能是任意的数字,它们必须是 tick spacing(刻度间隔) 的整数倍。tick spacing 在不同的池中是不一样的。不再提供流动性。在头寸的范围内,两种代币的流动性都存在,同样的超出这个范围,只有单个代币的流动性。

选择不同 tick spacing 的原因在于在 流动性集中度gas 消耗 之间进行权衡以应对不同交易对的波动性。每次在swap中跨越tick会产生gas消耗,所以tick跨越应尽可能少发生。波动性高的交易对,tick spacing应更大,减少tick频繁跨越。,但是也不能过大,否则流动性提供者(LP)无法精确聚焦于他们认为的价格区间 。

  • 高波动资产对(如 MEME/ETH):价格跳动大 → tick spacing 应更大;
  • 稳定资产对(如 USDC/USDT):价格稳定 → tick spacing 可更小,允许 LP 更精准布流。

较小的 tick spacing 意味着可以更精细地集中流动性,但也意味着在交换过程中 gas 消耗更高;而较大的 tick spacing 虽然牺牲了流动性集中度,但可以减少 gas 消耗。tick spacing 越大,每次迭代的步子越大,所需迭代次数就越少,从而 gas 消耗更低。当然,较大的 tick spacing 也有代价,就是流动性集中能力变差。 因为最小的可添加流动性的价格区间会变宽。

此外,还要考虑流动性提供者遭受无常损失的风险。高度波动的资产往往会导致更高的无常损失,而更稳定的资产往往会导致更低的无常损失。例如,稳定币对几乎没有无常损失的风险,而 meme 代币对则具有极高的风险。因此,LP 将要求更高的费用,以弥补为高度波动的资产提供流动性时产生的无常损失。同样,交易者可以容忍波动性资产的更高费用,因为交易这些资产的潜在回报要高得多。

这表明交易对波动性、tick 间距和费用之间的关系应如下所示:

  • 对于具有高无常损失风险的波动性交易对,tick 间距和费用都应较高,而对于具有低无常损失风险的更稳定的交易对,两者都可以较低。
  • 由用户决定哪些交易对更稳定或更不稳定。
  • 协议定义的是 tick 间距和费用层级之间的关系。

Factory 合约中的 TickSpacing 与 Fee 的设置

该关系记录在 UniswapV3Factory 合约中的一个映射:

1
2
mapping(uint24 => int24) public feeAmountTickSpacing;
function enableFeeAmount(uint24 fee, int24 tickSpacing) external onlyOwner

这个映射表示:给定 fee 费率,对应的 tickSpacing 是多少

通过函数enableFeeAmount() ,治理可以添加新的费率与 tick spacing映射关系。

最初的 fee ↔ tickSpacing 是在合约部署时通过构造函数初始化的。例如:

手续费 tier basis points tick spacing
0.01% 1 1
0.05% 5 10
0.3% 30 60
1% 100 200

什么是 basis points?

  • basis point(基点):1 basis point = 0.01%;
  • 所以 5 bp = 0.05%,30 bp = 0.3%,100 bp = 1%。

使用Factory合约创建新池的流程

用户创建池时需要提供:

  • 两个代币的地址
  • 期望的手续费tier

然后:

  • Factory会查feeAmountTickSpacing映射;
  • 得到tick spacing
  • feetickSpacing 一起传入Pool构造函数;
  • 并作为public immutable存储变量永久保存在池中。

创建池时无权限的(permissionless)

任何人都可以调用createPool()函数,只要该token pair + fee 组合尚未存在。因此,可以存在:

  • 相同token pair (如USDC/ETH)
  • 但不同手续费的多个池(如0.05%、0.3%、1%)
  • 由市场决定哪个池流动性最好

tick bitmap(刻度位图)

在 Uniswap V3 中,流动性头寸是通过一个下限 tick 和一个上限 tick 来定义的。这两个 tick 被保存在一个称为 tick bitmap(刻度位图) 的映射中。tickBitmap 是一个从 int16uint256 的映射(mapping)。
而一个 tick 是 int24 类型的数字,在 tickBitmap 中会被拆分为两个部分:

  1. 前16位(从左到右)会被转换为一个 int16,这部分称为 word position(字位置)。
  2. 后8位 会被转换为一个 uint8,称为 bit position(位位置)。

将 tick 存入 tick bitmap 的过程如下:

  1. 拆分 tick:int24 类型的 tick 拆分为 int16 的 word position(字位置) 和 uint8 的 bit position(位位置)。

  2. 访问 bitmap: 使用 word position 作为键,从 tick bitmap 映射中获取一个 uint256 类型的值。

  3. 构造掩码: 创建一个掩码(mask),这是一个长度为 256 的二进制串,只有某一个位置是 1,其他位置都是 0,这个 1 的位置就是 bit position。

  4. 异或操作: 用当前 tick bitmap 的值与掩码做异或运算(XOR):

    这样会将 bit position 对应的那一位翻转(0 变 1,1 变 0),其他位不变。

    • 如果该 bit 原来是 0,就表示我们添加了这个 tick;
    • 如果是 1,就表示我们移除了这个 tick。

从 tick bitmap 中读取 tick 的过程如下:

  1. 获取前16位: 也就是 tick bitmap 的键,对应的格式如:
  2. 获取 bitmap 值: 使用上面的键从 tick bitmap 映射中获取一个 uint256 的值
  3. 确定 bit 位置: 假设我们找到了某一个 bit 被设置了,比如第 7 个位置,那么我们把数字 7 写成 8 位的二进制:
  4. 组合成 tick: 将上面的 8 位二进制追加到前面 16 位的 tick bitmap 键上:得到的就是原始的 int24 类型的 tick 值。

在一次swap操作中用于寻找下一个tick

为了找到小于或等于当前 tick 的下一个初始化 tick,我们需要在 tick bitmap 的右侧进行查找。tick bitmap 的 key 是 int16 类型,在 Uniswap V3 的代码中被称为 word position。通过这个 key,我们可以获取一个 uint256 类型的值。我们希望寻找一个小于或等于当前 tick 的下一个初始化 tick。假设当前 tick 位于索引 7。如果我们在索引 7 的左侧查找为 1 的位,那我们其实是在寻找一个大于或等于当前 tick 的 tick。因此我们需要在索引 7 的右侧查找为 1 的位。

为了在索引 7 的右侧寻找值为 1 的位,我们首先创建一个掩码(mask)。这个掩码会在索引 7 处设置为 1,并且索引 7 右边的所有位也都设置为 1。我们会用 tick bitmap 中存储的 uint256 与这个掩码做按位与(bitwise AND)操作,从而得到一个新值,在索引 7 左边的位全部变成 0。换句话说,这个掩码和按位与的操作能帮助我们得到从索引 7 右边开始的所有 1。接下来我们需要在这个结果中找到最左边的 1(即最高有效位)。比如在这个例子中,我们假设只有在某个偏右的位置才出现第一个 1。我们查找这个结果中的最高有效位,其索引为 1。这就是下一个为 1 的位在右侧的索引位置。

现在我们已经知道了下一个 bit position,接下来就是计算下一个 tick。为了找到下一个 tick,我们从当前 tick 开始,先减去当前 bit position(用于清除低 8 位,使其全部变为 0),再加上新的 bit position。如果要找到“大于当前 tick 的下一个 tick”,思路和上面类似。但这次我们要在 tick bitmap 的左侧查找。

再次回到 tick bitmap 的 key(int16 类型),我们仍然使用 key 为 -784 的位置,它对应一个 uint256 值。本例中假设第 7 位是 0。现在我们需要判断应该在哪个方向查找值为 1 的位。如果向右查找,我们得到的是小于或等于当前 tick 的值。因此,为了找到大于当前 tick 的下一个 tick,我们需要在索引 7 的左侧查找为 1 的位。为此我们创建一个新的掩码:该掩码在索引 7 处为 1,其左侧的位全为 1,右侧的位全为 0。我们将 tick bitmap 中的 uint256 值与这个掩码进行按位与操作,这样我们就得到了一个右侧为 0 的结果值。接下来我们从这个结果中查找最低有效位(即最右边的 1)。

假设我们在索引 253 处找到了第一个为 1 的位。为了找到下一个 tick,我们从当前 tick 开始,减去当前 bit position(7),再加上下一个 bit position(253)以及 +1,确保结果大于当前 tick。

平方根价格(Square Root Price X96)

表示两个资产价格的平方根,而不是直接的价格。

sqrtPriceX96 是一个 Q64.96 固定点数,本质是 uint160(2^160^)。

其平方为 sqrtPriceX96^2,最大为 $2^{320}$

Uniswap 协议需要在 sqrtPriceX96 和 tick 索引之间进行双向转换。这个过程由 TickMath 库的以下两个函数处理:

  • getTickAtSqrtRatio:将 sqrtPriceX96 转换为 tick 索引
  • getSqrtRatioAtTick:将 tick 索引转换为 sqrtPriceX96

首先计算tick的绝对值,然后检查tick在合法范围内。接着通过平方乘算法用Q128.128格式计算tick为负数的时候:$\sqrt{Price} =\sqrt{ 1.0001^{\text{-|tick|}}}$ 如果原始tick为正数,则对结果取倒数即:$\Large {\frac{1}{\sqrt{1.0001^{-|tick|}}} = \sqrt{1.0001^{|tick|}}}$ 。然后将Q128.128值右移32位,转换成Q64.96格式,返回sqrtPriceX96

  • Uniswap V3 的 tick 范围是 [-887272, 887272]。由于 getSqrtPriceRatioAtTick 只直接计算负 tick,正 tick 通过倒数实现,因此只需处理 [-887272, 0] 区间。

  • 为了使用平方-乘算法,必须预先计算各个幂次的情形。Uniswap V3 在函数中预计算了以下形式的值:

    $\sqrt{1.0001^0}, \sqrt{1.0001^{1}},\ \sqrt{1.0001^{2}},\ \sqrt{1.0001^{4}},\ \ldots,\ \sqrt{1.0001^{2^{19}}}$ 887273 可由 20 位二进制表示(因为 $2^{20} = 1048576 > 887273$),这就是为何只预计算 20 个常量。

    Uniswap V3是将这些定点数 向上取整(除了 tick = -1)。

Uniswap V3 中价格用公式表达:
$$
\text{price} = \frac{\text{sqrtPriceX96}^2}{2^{192}}
$$
所以最大 price 为:
$$
\text{price}_{\max} = \frac{2^{320}}{2^{192}} = 2^{128}
$$
X96表示这个数值被放大了2^96^倍,用于在整数环境下保护高精度的浮点数(因为智能合约中不方便使用浮点数)

用公式表达就是
$$
\text{sqrtPriceX96} = \sqrt{Price} \times 2^{96}
$$

$$
\text{Price} = \left( \frac{\text{sqrtPriceX96}}{2^{96}} \right)^2
$$

$$
\text{Price} = 1.0001^{\text{tick}}
$$

由SqrtPriceX96 计算 Tick Index,然而这个公式仍不够精确,因为 SqrtPriceX96 是连续的浮点数,而 tick index 是离散的整数。因此我们需要 向下取整

$$
\text{tick} = \left\lfloor \frac{2 \cdot \log \left( \frac{\text{sqrtPriceX96}}{2^{96}} \right)}{\log(1.0001)} \right\rfloor
$$

其中由于对数换底公式$\log_b(a) = \frac{\log_k(a)}{\log_k(b)}$,对于tick的公式,不论你用自然对数、底2、底10……都不会影响最终结果。

$$
\text{sqrtPriceX96} = \sqrt{1.0001^{\text{tick}}} \times 2^{96}
$$

$$
\Large\text{sqrtPriceX96} = 1.0001^{\frac{\text{tick}}{2}} \times 2^{96}
$$

使用平方根价格可以简化计算,比如在计算流动性、价格变动时,乘法和除法变得更高效且更准确,在价格的变动范围很大时,用平方根价格可以更好地保持数值稳定性和精度。实际上,上述公式并非完全正确,因为sqrtPriceX96 是连续的值,而tick是离散整数,因此计算tick时必须向下取整(floor)。例如,若某价格位于两个 tick 之间,其对应的 tick 是靠下的那个。这也可以通过 Uniswap 的价格工具可视化看到:虽然价格是连续变化的,但 tick 始终是刚好小于价格的那一个整数值

Uniswap V3 实际上存储的是价格的平方根$\sqrt{P}\ $(存在槽0中,槽0存储的是一个结构体,里面包含平方根价格),而不是价格本身。这样的方法提高了gas效率。由于存储 tick index 占用的位数比存储价格要少,因此协议存储的是 tick index,而不是 tick 对应的实际价格。

Uniswap v3 流动性提供者(LP)的手续费

Uniswap V3的手续费是对输入代币收取的。假设Alice在一段时间内持续提供S的流动性,并且这段时间中发生了多次交换。同时,池中整体的流动性可能会发生变化,但Alice并没有增减她所持有的流动性,因此S保持不变。用fi表示在每次活动流动性为Li时,所收取的手续费。变量i的取值范围从0到N。则手续费为:$\Large f = S \cdot \sum_{i=0}^{n} \frac{f_i}{L_i}$ ,去掉流动性S,即为手续费增长(fee growth,即为fg) $\Large f_g = \frac{f_0}{L_0} + \frac{f_1}{L_1} + \cdots + \frac{f_N}{L_N}$。换句话说,我们把每次swap中收取的手续费除以当时的流动性,然后将所有这些结果相加。假设某次swap中收取了f0 的手续费,而当时的流动性是L0,那么这次贡献的fee growth就是$\Large\frac{f0}{L0}$。将所有swap的情况从i = 0 加到N,我们可以简写成如下求和形式:$$\Large\sum_{i=0}^{N} \frac{f_i}{L_i}$$

fee growth 是 Uniswap V3 用于追踪手续费分配的核心状态变量;它按每次 swap 收取的手续费除以流动性累积而成;根据 swap 的方向,fee growth 的轨迹会呈现为折线;随着价格的变化,fee growth(手续费增长)有时会超出这个范围,有时则处于范围之内。那么,在这种情况下我们计算实际收取的手续费的关键思想是:计算在流动性头寸价格范围内的 fee growth,并将其乘以对应的流动性数量。我们可以将一段的 fee growth 想象成一条橙色的线,其高度就代表这个头寸内部的手续费增长。手续费的变化和输入的代币有关,若fee growth不变,tick会向左移动,因为输入代币被换入(相对来说被换入的更多),价格下降,若fee growth 增加,tick向右移动则说明有更多的输入代币换出,价格上升。

将 tick 以下手续费增长 $f_b$ 表达式:

$$\Large f_b =\begin{cases}f_{o,i}, & \text{若 } i \le i_c \f_g - f_{o,i}, & \text{若 } i> i_c\end{cases}$$

tick 上方手续费增长 $f_a$ 表达式:

$$\Large \begin{equation}
f_a =
\begin{cases}
f_g - f_{o,i}, & \text{若 } i \le i_c \
f_{o,i}, & \text{若 } i > i_c
\end{cases}
\end{equation}$$

假设 $S$ 是该仓位在 tick 区间 $[i_{\text{lower}}, i_{\text{upper}}]$ 上提供的流动性,且在整个过程中保持不变。

那么,这个仓位应得的手续费为:
$$
\text{应得手续费} = S \cdot (F_1 - F_0)
$$
其中:

  • $F_0$:仓位创建时的手续费增长快照;
  • $F_1$:手续费提取时的快照;
  • $S$:仓位在此区间内提供的流动性;
  • $F_1 - F_0$:单位流动性所获得的手续费增长。

Uniswap V3 中的 TWAP(时间加权平均数)

在 Uniswap V3 中,时间加权平均价格(TWAP, Time Weighted Average Price) 是通过几何平均数来计算的。
这与 Uniswap V2 不同,V2 是通过对每一秒的价格求和,然后除以时间长度来计算 TWAP,本质上是使用算术平均数。在 Uniswap V3 中,TWAP 使用价格的几何平均数计算,具体操作是:将每一秒的价格相乘,然后对这些乘积取 $n$ 次方根(其中 $n$ 是价格的数量)。

假设我们在时间区间 $[t_1+1, t_2]$ 中每秒钟的价格分别为 $P(t_1+1), P(t_1+2), \dots, P(t_2)$。

那么 TWAP 可以记作 $P_{\text{avg}}$,计算公式为:
$$
P_{\text{avg}} = \sqrt[n]{P(t_1+1) \cdot P(t_1+2) \cdots P(t_2)}
$$
其中,$n = t_2 - t_1$,是区间内的秒数。

如何计算项数

比如时间区间是:

  • $t_1+1, t_1+2$:两项
    $$
    t_2 - (t_1+1) + 1 = 2
    $$

  • $t_1+1, t_1+2, t_1+3$:三项
    $$
    t_1+3 - (t_1+1) + 1 = 3
    $$

所以一般来说,项数为:
$$
n = t_2 - t_1
$$
在智能合约中,直接进行大量乘法开销太大。因此 Uniswap V3 引入了一个状态变量 tick 累加器,记作 tick cumulative。我们用 $a(t)$ 表示 tick 累加器,在时间 $t$ 的值是从合约部署开始每秒 tick 的和:
$$
a(t) = \sum_{i=0}^t T(i)
$$

将 TWAP 表达式替换为 tick 表达式

从最初的 TWAP 几何平均表达式开始:
$$
P_{\text{avg}} = \sqrt[n]{P(t_1+1) \cdot P(t_1+2) \cdots P(t_2)}
$$
使用 $P = 1.0001^T$ 替换:
$$
P_{\text{avg}} = \sqrt[n]{1.0001^{T(t_1+1)} \cdot 1.0001^{T(t_1+2)} \cdots 1.0001^{T(t_2)}}
$$
利用幂运算法则(相同底数乘积 → 指数相加):
$$
P_{\text{avg}} = \left(1.0001^{T(t_1+1) + T(t_1+2) + \cdots + T(t_2)}\right)^{1/(t_2 - t_1)}
$$
等价于:
$$
P_{\text{avg}} = 1.0001^{\frac{\sum_{i = t_1+1}^{t_2} T(i)}{t_2 - t_1}}
$$
我们已知:
$$
a(t_2) = \sum_{i=0}^{t_2} T(i), \quad a(t_1) = \sum_{i=0}^{t_1} T(i)
$$
所以:
$$
\sum_{i = t_1+1}^{t_2} T(i) = a(t_2) - a(t_1)
$$
最终 TWAP 表达式化简为:
$$
P_{\text{avg}} = 1.0001^{\frac{a(t_2) - a(t_1)}{t_2 - t_1}}
$$
这个指数部分:
$$
\frac{a(t_2) - a(t_1)}{t_2 - t_1}
$$
称为时间加权平均 tick 值(TWAT),是计算 TWAP 的关键中间值。

其中:

  • $a(t)$:tick 累加器
  • $T(i)$:第 $i$ 秒的 tick 值
  • $P$:价格
  • $T$:tick 值

计算Token X和Token Y的TWAP

我们设定:

  • $P_x$:Token X 的现价(以 Token Y 表示)
  • $P_y$:Token Y 的现价(以 Token X 表示)

例如:若 Token X 为 ETH,Token Y 为 USDC,则:
$$
P_y = \frac{1}{P_x}
$$
无论是 Uniswap V2 还是 V3,现价的倒数关系始终成立

设:

  • $P_x^{avg}$:Token X 的时间加权平均价格
  • $P_y^{avg}$:Token Y 的时间加权平均价格

Uniswap V2 中
$$
P_y^{avg} \ne \frac{1}{P_x^{avg}}
$$
但在 Uniswap V3 中
$$
P_y^{avg} = \frac{1}{P_x^{avg}}
$$
在 V3 中,我们通过 tick 累积器计算 TWAP。

  • 价格与 tick 的关系:

$$
P = 1.0001^T \quad \Rightarrow \quad T = \log_{1.0001} P
$$

令:

  • $T_i$:第 $i$ 秒的 tick 值
  • $P_{x,i} = 1.0001^{T_i}$
  • $P_{y,i} = \frac{1}{P_{x,i}} = 1.0001^{-T_i}$

若我们计算 $n$ 秒内的平均 tick:
$$
T_x^{avg} = \frac{1}{n} \sum_{i=1}^n T_i
$$
对应的 Y 的 tick:
$$
T_y^{avg} = \frac{1}{n} \sum_{i=1}^n (-T_i) = -T_x^{avg}
$$


然后:
$$
P_x^{avg} = 1.0001^{T_x^{avg}}, \quad P_y^{avg} = 1.0001^{T_y^{avg}} = 1.0001^{-T_x^{avg}} = \frac{1}{P_x^{avg}}
$$
这就严格证明了在 Uniswap V3 中
$$
P_y^{avg} = \frac{1}{P_x^{avg}}
$$

Uniswap V3 Factory Contact Calls

在Uniswap V3中,使用create2指令部署Pool合约。由于这一特性,合约地址可以在合约尚未部署之前被确定。但是Uniswap V3 工厂合约部署Pool合约的方式并不直接。

一般的create2计算合约地址都是

keccak256(0xff ++ 部署者地址 ++ salt ++ keccak256(创建字节码 + 构造参数))

计算完成后,取结果的最后20个字节,即合约地址。

  • 0xff:固定的前缀,用于区分 createcreate2
  • 部署者地址:部署合约的账户地址,这里是工厂合约地址。
  • salt:由部署者指定的 32 字节随机数。
  • 创建字节码的 keccak256 哈希:这是合约的创建码,包含合约逻辑和构造函数逻辑。

其中,若部署的是同一个合约,那么创建字节码固定。构造参数和 salt 可变,从而控制地址的不同。

而在Uniswap V3中,Pool合约不带构造函数参数。这意味着我们只能通过salt来使每个Pool地址唯一。

我们不能把所有初始化参数都放进 salt,因为 salt 只能包含 token0、token1 和 fee。我们也不能将 factory 地址或 tick spacing 放入构造参数,因为这样无法保证地址只由 token0、token1 和 fee 决定。

地址唯一性目标

  • salt 设置为:keccak256(token0, token1, fee)
  • 不使用构造函数参数

初始化数据目标

  • 将初始化数据(token0、token1、fee、factory 地址、tick spacing)暂时存储在工厂合约中

Uniswap V3 的Swap

在 Uniswap V3 中,执行 swap(交换)操作的函数位于名为 Uniswap V3 Pool 的合约中,该函数就叫做 swap

swap 函数的关键输入参数如下:

  • **zeroForOne**:一个布尔值,指示交易方向。
    • 如果为 true,表示从 token0 交换成 token1;
    • 否则表示从 token1 交换成 token0。
  • **amountSpecified**:一个数值,可能为正或负,表示用户希望输入(正数)或输出(负数)的 token 数量。
  • **sqrtPriceLimit**:交换过程中的价格上限(或下限),使用价格的平方根表示。

接着算法会进入一个 while 循环,不断计算输入输出量。循环中使用的变量包括:

  • amountSpecifiedRemaining:初始化为 amountSpecified
  • sqrtPriceCurrent:当前的价格(平方根)。

循环持续条件:

  • amountSpecifiedRemaining ≠ 0,即还有 token 没处理完;
  • 当前平方根价格 sqrtPrice 未达到 sqrtPriceLimit

while 循环继续的条件是:

  • amountSpecifiedRemaining ≠ 0
  • sqrtPriceCurrent ≠ sqrtPriceLimit

在每一轮迭代中:

  1. 读取下一个 tick;
  2. 计算 sqrtPriceNext
  3. 使用 computeSwapStep() 函数计算:
    • amount in
    • amount out
    • fee
  4. 更新价格;
  5. 根据交换类型更新剩余输入量或输出目标。

最终返回:

  • amountSpecifiedRemaining
  • amountCalculated

根据这些值,计算最终 token0 和 token1 的输入输出量。

循环体的处理流程如下:

  1. 检索下一个 tick
  2. 计算该 tick 对应的价格平方根 sqrtPriceNext
  3. 在当前价格到 sqrtPriceNext 范围内,调用 computeSwapStep() 计算:
    • 输入量(amount in)
    • 输出量(amount out)
    • 手续费(fee)
  4. 更新当前价格到 sqrtPriceNext
  5. 根据交换类型更新:
    • 对于 exact input
      • amountSpecifiedRemaining -= (amountIn + fee)
      • amountCalculated = amountOut
    • 对于 exact output
      • amountSpecifiedRemaining += amountOut
      • amountCalculated = amountIn + fee
  6. 更新以下局部变量:
    • 当前流动性
    • 当前价格平方根
    • 当前 tick
    • 累积手续费增长量(feeGrowth)

while 循环结束时,算法将拥有:

  • 最新的流动性
  • 最新的价格平方根
  • 最新 tick
  • token0 和 token1 的输入/输出数量
  • 总手续费

然后,合约更新全局状态,包括:

  • 流动性
  • sqrtPrice
  • tick
  • 手续费增长变量(feeGrowth)

接下来触发 swap 回调(swapCallback),由 msg.sender(必须是合约)响应,并将 token 发送到 Uniswap V3 Pool 合约。
最后,Uniswap V3 池子合约会检查收到的 token 是否足够。

执行核心计算的函数:computeSwapStep()

如果是 exact input,计算从 sqrtPricesqrtPriceTarget 范围内可接收的最大输入量。

如果是 exact output,计算最大可输出量。

计算交易后会移动到的新价格:sqrtPriceNext

假设你执行一个 zeroForOne 的交换,即:token0 输入、token1 输出。

  • 当前价格是 sqrtPrice
  • 目标价格是 sqrtPriceTarget

随着 token0 输入,价格从右向左移动,流动性会增加 token0、减少 token1。

  • max amount in:在价格未突破 sqrtPriceTarget 的前提下,最多可输入多少 token0。
  • max amount out:在价格未突破 sqrtPriceTarget 的前提下,最多可输出多少 token1。

sqrtPriceNext 是由于本次交易产生的价格变化后,新的价格平方根位置。

例如,一开始的 sqrtPrice 在右边,在输入 token0 之后,新的价格平方根位置变为左边,这个新的位置就是 sqrtPriceNext

computeSwapStep() 会返回:

  • sqrtPriceNext
  • amountIn
  • amountOut
  • feeAmount

这些数据用于更新池子状态并最终完成交换逻辑。

平方与乘算法(Square and Multiply Algorithm)

平方与乘算法是一种以对数时间复杂度O(logn)计算整数幂的方法。可以简单理解为将底数连续乘以自身n次,即直接计算a^n^的方法需要执行n-1次乘法运算,因此时间复杂度是O(n)。Uniswap V3 使用了平方与乘法算法来将 tick 索引转换为平方根价格。

假设我们希望计算 $a^{20}$,而不是将 $a$ 连续乘以自身 20 次,我们可以如下操作:

  1. 从 $a$ 开始
  2. 不断平方(而不是简单相乘)
  3. 最终组合出 $a^{20}$

例如:

  • $a^2 = a \cdot a$
  • $a^4 = (a^2)^2$
  • $a^8 = (a^4)^2$
  • $a^{16} = (a^8)^2$
  • 然后:
    $a^{20} = a^{16} \cdot a^4$

这个幂的“加法组合”方式,源自代数学中的幂的乘法法则(product-of-powers rule):

$a^m \cdot a^n = a^{m+n}$

平方指数序列(Squared Exponents Sequence)

“平方指数序列”这一术语,指的是如下形式的序列:其中每一项都是前一项的平方,底数 $a$ 可以是任意非零值。
$$
[a^1, a^2, a^4, a^8, a^{16}, \dots]
$$

小数底数的平方与乘法(Fractional Base)和分数指数的平方与乘法算法(Fractional Exponents)

如果我们提前知道底数是固定值,且所有可能的幂次有一个已知的最大值,我们可以预先计算平方指数序列并缓存数值,之后只需查表(而不是重新计算乘法)即可。使用缓存方法时,我们必须预先知道应用程序中可能出现的最大指数值。分数指数的平方与乘算法本质上仍是一个整数幂的计算,指数只是换了表达方式。

因此,即使指数是小数(或分数),只要它是整数除以常数,我们也可以使用相同的策略预计算平方指数序列。

如何在平方指数序列中挑选元素

如果我们希望计算a^k^,那我们如何从已缓存的平方指数序列中选择正确的项。

方法一:

最朴素的方式是:从最大的预计算幂开始往下遍历,每次减去当前项,知道总和等于目标幂。尽管有效,但这种方式是线性的,性能不理想。

最好的方式是使用二进制表示法

与上述的线性查找方法不同,我们可以更高效地利用一个关键观察:目标指数的二进制表示可以准确告诉我们需要从平方指数序列中选择哪些元素。

二进制转十进制回顾

要将一个二进制数转换为十进制数,我们使用如下公式,其中
$B_i$ 是该二进制数的第 $i$ 位(从右往左,第 0 位开始):
$$
\text{Decimal} = B_0 \cdot 2^0 + B_1 \cdot 2^1 + B_2 \cdot 2^2 + \cdots
$$
例如:

  • 13 的二进制是 1101,因为 $8 + 4 + 0 + 1 = 13$
  • 52 的二进制是 110100,因为 $32 + 16 + 0 + 4 + 0 + 0 = 52$

因此,如果我们要计算 $B^{13}$,而平方指数序列是
$[B^1, B^2, B^4, B^8, B^{16}, …]$,我们只需查看 13 的二进制表示为 1101,即可得出我们需要取:

  • 第 3 位:$2^3 = 8$
  • 第 2 位:$2^2 = 4$
  • 第 0 位:$2^0 = 1$

然后计算:
$$
B^{13} = B^8 \cdot B^4 \cdot B^1
$$
因为这几个幂次的乘积等于 $B^{13}$。

如何检测某一位是否为1:

1
2
3
function nthBitSet(uint256 x, uint8 n) public pure returns (bool isSet) {
isSet = x & (uint256(2)**n) != 0;
}

这段代码的原理如下:

  • uint256(2)**n 会创建一个只有第 n 位为 1 的数字。例如 2**3 = 8,其二进制为 1000
  • 如果 x & (2**n) 的结果不为 0,说明 x 的第 n 位也为 1

二进制与运算的原理

只有两个对应的位都为 1 时,位与操作(bitwise AND)才返回 1,否则返回 0。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x      = 1101  (13 的二进制)
2**3 = 1000 (8 的二进制)
--------------------------
x & 8 = 1000 => 非 0,返回 true

x = 1101
2**1 = 0010
--------------------------
x & 2 = 0000 => 返回 false

x = 1101
2**0 = 0001
--------------------------
x & 1 = 0001 => 返回 true

实际应用中:使用定点数的平方-乘法算法

在智能合约等实际系统中,通常不能使用浮点数,而是使用定点数(fixed-point numbers,又称 Q-numbers)。

例如:两个 18 位小数的定点数相乘,结果必须除以 $10^{18}$ 进行归一化,否则结果会有 36 位小数。将 Q128 定点常量用于乘法,然后右移 128 位归一化

为什么不直接使用指数运算的opcode

当我们将一个整数提升到另一个整数的幂时,通常使用虚拟机(如EVM)内置的指数运算指令(opcode)是更高效的做法。然后这个opcode 并不包含我们前面提到的“归一化步骤”。因此如果a^b 中的a或b中至少有一个是定点数,那么我们就不能使用虚拟机内置的exp指令来完成运算。

Q数格式(Q Number Format)

Q数格式是一种用于描述二进制定点数(fixed-point number)的记法

Solidity中定点数的使用背景

由于Solidity不支持浮点数(floating point),所以我们常用定点数来存储小数部分。实现方式:将小数乘以一个整数,使其变成整数来存储(这可能会带来精度损失)因为小数部分只能精确表示2 的幂分数,无法精确表示无法精确表示像 1/3 这样的十进制分数。

Q数使用2的幂而不是10的幂进行放大,因为在EVM中,乘/除以2 的幂可以用移位操作来完成(gas更省):

  • x << n 等价于 x * 2^n^
  • x >> n 等价于 x / 2^n^

Q数格式结构

Q数通常写作Qm.n,表示:

  • m是整数部分所占的位数(bits)
  • n是小数部分所占的位数(bits)

总位数是m+ n, 在Qm.n中表示x: x << 2^n^ ,即x*2^n^ 。最大可表示值为:

  • 最大整数部分:2^m^-1
  • 最大小数部分:1- 1/2^n^
  • 总最大值约为 2^m^ - 1 + (1- 1/2^n^) = 2^m^ - 1/2^n^

Q数格式实际存储位宽要求:

  • Q64.64 最少需要 uint128
  • Q64.96 最少需要 uint160
  • Q128.128 需要 uint256

Q 数加(减)法

要对两个 Q 数相加(减),它们必须具有相同的小数位数(即 n 相同),否则需要通过位移对齐小数点。

Q 数乘法

两个 Q 数相乘后,结果需右移 n 位,以还原正确的定点格式

product = (a * b) >> n;

Q 数除法

两个 Q 数相除,先将分子左移 n 位

quotient = (a << n) / b;

Uniswap V3 的缺点

Uniswap V3 更像是“为专业做市商设计的 DeFi 内核”,而不是为普通用户设计的 AMM。它不是失败,而是 立场明确的取舍

对 LP(流动性提供者)不友好的地方

  • 集中流动性 ≠ 被动收益

    V3 最大创新是 Concentrated Liquidity,但代价是:

    • LP 必须主动管理头寸

    • 价格一旦跑出区间 → 资金 0 利用率

    • 本质上变成了:

      “存钱吃利息”
      “做市 + 盯盘 + 调参”

    结果:

    • 适合 专业做市商 / Bot
    • 普通用户 LP 收益反而可能不如 V2
  • 无常损失(IL)被“放大”

    V3 不是消灭 IL,而是:

    • 在窄区间内 → 手续费更高
    • 但一旦价格剧烈波动:
      • IL 更集中
      • LP 更容易在“错误价格”被成交

    结论:
    V3 是高杠杆版 LP,赚得快,也亏得快。

  • 头寸碎片化,管理成本高

    V3 的 LP Position 是:

    • NFT(ERC721)
    • 每个区间、费率都是一个独立头寸

    问题来了:

    • 无法像 ERC20 那样:
      • 组合
      • 自动复利
      • 跨协议通用

    所以才催生了一堆:

    • Uniswap V3 Manager
    • Vault / Rebalancer
    • Gamma / Arrakis / Alpaca

    但这 = 额外信任 & 额外合约风险

对普通交易者的缺点

  • 路由复杂,滑点更“不可预期”

    V3 存在多个:

    • fee tier(0.01 / 0.05 / 0.3 / 1%)
    • 离散 tick
    • 非连续流动性

    导致:

    • 同一交易对,不同池子价格不同
    • 路由器必须:
      • 拆单
      • 多跳
      • 多池

    后果:

    • 用户自己几乎无法判断最优价格
    • 必须依赖 Router / Aggregator

  • MEV 空间反而更大

    集中流动性带来的副作用:

    • 深度集中在少数 tick
    • 大额 swap 更容易:
      • 推价格
      • 被 sandwich

    而且:

    • LP 常在关键价格附近设区间
    • 这些“明显的流动性边界”, 非常容易被 MEV Bot 利用

协议 & 工程层面的缺点

  • 合约复杂度极高(审计成本巨大)

    V3 的特点是:

    • 重度定点数运算
    • 位运算 + packed storage
    • tick bitmap + 边界扫描
    • gas 优化到“不可读”

    风险:

    • 极难审计
    • 极难复刻
    • 极难二次开发

    相比之下:

    • V2 ≈ 教科书
    • V3 ≈ 工业级内核
  • 不利于 L2 / 高频场景

    虽然 V3 在 L2 上也部署了,但:

    • LP 调整区间 = 多次交易
    • mint / burn / collect 都是重操作
    • tick crossing gas 不稳定

    高频、自动化策略在:

    • 低 gas L2 才勉强可行
    • 在 L1 基本不可持续

生态与可组合性问题

  • NFT LP 破坏 DeFi 可组合性

    ERC721 LP 带来的问题:

    • 无法直接:
      • 作为抵押品
      • 放进 AMM
      • 参与标准化收益协议

    所以你会看到:

    • Aave / Compound 基本不原生支持 V3 LP
    • 必须“包一层” → ERC20 Vault Token

    这在 DeFi 世界里是倒退了一步

战略层面的缺点(设计取舍)

  • 更像 TradFi 做市,而不是 DeFi 普惠

    V3 的设计哲学其实是:

    CEX 的专业做市模型
    移植进 链上

    结果是:

    • 专业机构:收益↑
    • 普通用户:理解成本↑、风险↑
    • DeFi 的“permissionless 被动收益”属性被削弱