Uniswap v3 中的 Position

本文深入探讨了Uniswap V3中添加流动性的机制,重点介绍了position的概念以及如何在UniswapV3Pool合约中使用mint函数来增加流动性。文章还详细解释了positions映射如何存储position信息,以及如何计算position所需的token数量,最后说明了mint函数需要通过position manager合约来调用。

向 AMM 添加流动性意味着将 Token 存入 AMM 池。流动性提供者这样做,是希望从与该池进行交换的用户那里赚取费用。

在 Uniswap v2 中,当 LP 添加流动性时,他们会收到池的份额,以流动性提供者 Token(LP Token)的形式表示,代表他们有权获得的池中 Token 的百分比,包括费用。这些 LP Token 是同质化的 ERC-20 Token。

在 Uniswap v3 中,这种方法行不通,因为 LP 选择他们想要存入流动性的范围——下限和上限的 tick。因此,协议需要以非同质化的方式单独跟踪每个存款。这就引出了仓位的概念。

当 LP 在一个范围内添加流动性时,我们说他们打开或修改了一个仓位。当仓位尚不存在时,它是打开的;当它已经存在时,它是修改的。

一个范围由两个 tick 组成——一个下限 tick 和一个上限 tick。将流动性存入一个范围意味着增加该范围内的实际储备。这是通过 UniswapV3Pool.sol 中的 mint 函数实现的,其接口如下所示。

// UniswapV3Pool.sol

function mint(
  address recipient, // 仓位的拥有者
  int24 tickLower,
  int24 tickUpper,
  uint128 amount, // 流动性数量
  bytes calldata data // 将在以后的章节中解释,对我们的讨论不是必要的
) external override lock returns (uint256 amount0, uint256 amount1) {

这个函数的名称 mint 让人想起 Uniswap v2,在 Uniswap v2 中,添加流动性会为流动性提供者 铸造 ERC-20 Token。虽然这在 v3 中不再发生,但名称被保留了下来——这次指的是 铸造一个仓位。

本章的目标是研究协议如何以及在哪里存储有关这些仓位的信息。

positions 映射

仓位存储在一个名为 positions 的映射中,该映射位于 UniswapV3Pool 合约中,如下图所示。

positions_mapping.png

标识仓位的key由仓位所有者的地址、下限 tick 和上限 tick 的 Keccak 哈希值构成。

因此,如果这是所有者 0xA 第一次在 tick -10 和 10 之间存入流动性,将创建一个 keykeccak(0xA, -10, 10) 的仓位。

下面是用 Solidity 代码生成仓位 key 的示例。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Position {

    function getKey(
        address owner,
        int24 tickLower,
        int24 tickUpper)
    public pure returns (bytes32 key) {
        key = keccak256(abi.encodePacked(owner, tickLower, tickUpper));
    }

}

一旦创建,如果在所有者 0xAtick -10 和 10 之间存入(或提取)更多流动性,则将修改该仓位,因为它已经存在。

映射的值类型为 Position.Info,它是一个名为 Infostruct,位于 Position.sol 合约内的 Position 库中。

这个 struct(如下所示)存储了仓位的 liquidity(橙色框),以及与仓位拥有的费用和 Token 相关的其他四个字段(绿色框)。我们将推迟对这些其他字段的讨论,直到我们讨论从仓位中提取费用和流动性。

positions1.png

目前,我们可以这样理解:如果某个所有者在一个范围内添加了流动性,那么就会创建一个仓位。之后,可以通过添加或移除流动性来修改此仓位。

仓位是非同质化的

在 Uniswap v2 中,当 LP 提供流动性时,会为 LP 铸造 ERC-20 LP Token。这些 LP Token 是同质化的,代表了池资产的一部分。这意味着,如果两个 LP 各自拥有 1000 个 LP Token,并使用他们的 LP Token 赎回他们在池中的份额,那么他们收到的资产 Token 将是相同的。

在 Uniswap v3 中,仓位是非同质化的。这意味着,如果两个 LP 具有不同的仓位,并使用这些仓位赎回他们在池中的份额,那么他们可能不会收到相同数量的资产,因为它们可能不代表相同的 tick 范围,或者 LP 在这些 tick 之间贡献的实际储备。

可以通过外围合约将这些非同质化的仓位作为 ERC-721 非同质化 Token 进行处理,但核心合约本身不会这样做——它不是 ERC-721 合约,也不允许将仓位转移给第三方。核心合约只允许打开和修改仓位。

Uniswap v3 中的费用

Uniswap v3 中的费用运作方式也与 Uniswap v2 非常不同。在 Uniswap v2 中,每当发生交换时,费用都会被添加到池中,从而增加每个份额有权获得的 Token 数量。

在 Uniswap v3 中,一个仓位只有权获得在其 tick 范围内(部分或全部)发生的交换的费用。因此,如果 LP 打开一个从未参与交换的仓位,那么该仓位将不会获得任何费用。这激励 LP 在可能发生交换的区域打开仓位,从而在最需要的地方增加流动性。

由于费用不再在所有 LP 之间按比例共享,而是单独分配给每个仓位,因此必须单独跟踪它们。

协议如何计算一个仓位有权获得的费用数量并非易事,它涉及多个变量的组合,包括仓位的 Info struct 中存在的 feeGrowthInside0LastX128feeGrowthInside1LastX128 变量。

作为一种高层次的解释,协议会跟踪自创建以来池中累积的费用,以及在每个用作仓位边界的 tick 上下收集了多少费用。

这使得协议能够计算出在仓位的下限 tick 和上限 tick 之间收集了多少累积费用,并使用仓位的 feeGrowthInside0LastX128feeGrowthInside1LastX128 变量来确定这些费用中有多少属于该特定仓位。

协议如何实现这一点的细节将在以后的章节中介绍。

一个池由多个仓位组成

我们一直在说一个池由多个段组成,所以我们需要将段和仓位的概念联系起来。它们并不相同,因为仓位可以重叠。让我们通过一个例子来探讨这一点。

仅考虑两个仓位:

  1. tick -10 和 5 之间,流动性为 200,如下图中的红色框所示。
  2. tick 0 和 10 之间,流动性为 100,如下图中的蓝色框所示。

A diagram showing how positions relate to liquidity levels between ticks

  • tick -10 和 0 之间,只有一个仓位,流动性为 200,因此该段的流动性将为 200。
  • tick 0 和 5 之间,两个仓位重叠:一个流动性为 200,另一个流动性为 100,因此该段的流动性将为 300。
  • tick 5 和 15 之间,只有一个仓位,流动性为 100,因此该段的流动性将为 100。

这些段显示在上图的右侧。

下面是一个交互式工具,读者可以在其中创建多个仓位,该工具会自动根据这些仓位计算段。还可以更改当前价格,并查看当前价格所在的段的流动性。

Uniswap V3 仓位和段

Uniswap V3 仓位和段

此交互式工具允许你通过添加流动性来打开和修改仓位。

要创建仓位,请选择下限 tick、上限 tick 以及你要添加的流动性数量。如果 该仓位不存在,它将被创建;如果它已经存在,它的流动性将被更新。 你可以创建多个仓位。

此工具包含两个图表:第一个显示由仓位生成的段,第二个 显示所有已创建的仓位。

你可以移动价格滑块(tick)以调整两个图表中的价格并查看流动性 对应于当前价格。

X

Y

-50

-40

-30

-20

-10

0

10

20

30

40

50

0

价格 0 (流动性:0)

-50

-40

-30

-20

-10

0

10

20

30

40

50

价格:0

预览:-30 到 30 | L: 20

下限 Tick -30

上限 Tick 30

流动性 20

创建仓位重置所有

但是,协议不会像我们刚才那样计算所有基于仓位的段。这将非常低效,因为协议不需要这样的全局视图。

在后面的章节中,我们将讨论协议如何有效地处理仓位以计算段。目前,我们暂时将该计算视为一个黑盒。

在下一节中,我们将更仔细地研究 mint 函数以及 LP 必须存入什么才能打开一个仓位。

mint 函数

要向池中添加流动性,必须存入 Token。正如我们所看到的,这是通过 mint 函数完成的,其接口再次显示如下。

function mint(
  address recipient, // 仓位的拥有者
  int24 tickLower,
  int24 tickUpper,
  uint128 amount, // 流动性数量
  bytes calldata data // 将在以后的章节中解释,对我们的讨论不是必要的
) external override lock returns (uint256 amount0, uint256 amount1) {

此函数需要五个参数:

  • 仓位所有者的地址 (recipient),仓位的下限 tick (tickLower) 和上限 tick (tickUpper)。
  • 类型为 bytesdata 参数将在稍后解释,对于当前的讨论并不重要。
  • amount 参数,它是 LP 想要在下限 tick 和上限 tick 之间添加到池中的流动性数量

请注意,amount 的类型为 uint128,这意味着它不能为负数。mint 函数仅用于添加流动性,而不是移除流动性——移除由 burn 函数处理,这将会在稍后讨论。

然后,需要由 mint 函数计算出添加此 amount 的流动性所需的 Token 数量。

这就是我们接下来要看到的。

打开仓位所需的 Token

与下限 tick 和上限 tick 之间的流动性相对应的 Token 数量是对应于该仓位的段的实际储备,即,一个流动性为 的下限 tick 和上限 tick 之间的段。

我们已经知道,一个段的实际储备不仅取决于 tick 的边界和流动性 ,还取决于当前价格。规则如下:

  1. 当当前价格等于或高于上限 tick 时,该段只有 Y Token 的实际储备。
  2. 当当前价格等于或低于下限 tick 时,该段只有 X Token 的实际储备。
  3. 当当前价格在下限 tick 和上限 tick 之间时,该段同时具有 X Token 和 Y Token 的实际储备。

下图说明了这三种情况,其中红线代表当前价格 , 代表下限 tick, 代表上限 tick

A diagram showing how when the price is higher than the upper tick of the range, the reserves are entirely token y and when the price is below the range, the reserves are all token x. When the price is in between, both tokens make up the liquidity in the range.

如果 LP 想要在下限 tick 和上限 tick 之间添加流动性 ,协议会根据上述三种情况计算 (X Token 的实际储备)和 (Y Token 的实际储备)。

  1. 对于情况 1,Y Token 的实际储备为

  2. 对于情况 2,X Token 的实际储备为

  3. 对于情况 3,X Token 和 Y Token 的实际储备为

添加流动性 到仓位所需的这些 Token 数量由 mint 函数计算并返回,如下图中的红色框所示。

The return signature of mint

然而,对于最终用户来说,流动性可能是一个高度抽象的概念。最终用户更常考虑 Token 而不是流动性——例如,用户可能想通过存入 100 个 X Token 来提供流动性,而不知道 100 个 X Token 代表的流动性数量。

可以通过一个充当最终用户和核心合约之间的桥梁的中介合约来促进通过选择 Token 数量来添加流动性,该合约计算 Token 数量和流动性之间的转换。

通过仓位管理器打开仓位

核心合约中的 mint 函数旨在由另一个合约调用,而不是由 EOA 调用。

当调用 mint 函数时,它会通过 uniswapV3MintCallback 函数回调触发它的地址,正如我们在下面的 mint 函数代码片段中看到的,它高亮显示在一个红色框中。

The line of code in mint where the callback happens

调用 mint 函数的地址必须实现 uniswapV3MintCallback 函数,因为这是调用者必须转移修改仓位所需的 Token 数量的时候。mint 函数在调用 uniswapV3MintCallback 之前和之后立即检查池的余额,并计算差额。

这就是为什么 mint 函数不能被 EOA 调用的原因:EOA 无法响应该回调以转移所需的 Token 数量。因此,如果 LP 需要转移任何 Token,EOA 对 mint 函数的调用将恢复,这始终是这种情况(不允许通过添加零流动性来修改仓位)。

交易流程如下图所示,假设调用 mint 函数的合约名为仓位管理器。

A visual representation of the Position manager and Uniswap V3 Core

核心合约对用户不友好。一般来说,LP 想要选择他们希望存入以打开仓位的 X Token 和/或 Y Token 的数量,而不是指定流动性。中介合约的作用是提供一个用户友好的界面,用户在其中选择一个 Token 数量,仓位管理器将此转换为该数量的相应流动性。

此过程的表示如下图所示。最终用户(EOA)调用中介合约(仓位管理器)中的 mint 函数,并将他们想要转换为流动性的 Token 数量作为参数之一传递。然后,仓位管理器将此 Token 数量转换为流动性,并通过调用核心合约中的 mint 函数为最终用户打开一个仓位。

A diagram showing how users interact with the position manager

我们不会在本书中详细介绍仓位管理器的工作原理。Uniswap 提供了一个作为仓位管理器的合约,名为 NonfungiblePositionManager,位于其 外围库中,该库位于与 核心库 不同的存储库中。

总结

  • 在 Uniswap v3 中添加流动性意味着打开或修改一个仓位。仓位由其所有者的地址及其下限和上限 tick 定义。
  • 仓位存储在一个名为 positions 的映射中,其 key 是所有者地址以及下限和上限 tick 的 Keccak 哈希值,其值是位于 Position 库中的 struct
  • 要打开或向仓位添加流动性,使用 mint 函数。此函数对用户不友好,并接收作为参数的,用户希望在下限和上限 tick 之间存入的流动性数量。
  • mint 函数必须由其他合约调用,而不是由 EOA 调用。这些中介合约充当 EOA 和核心合约之间的桥梁,它们的功能之一可能是将 LP 定义的 Token 数量转换为核心合约的 mint 函数期望的流动性数量。
  • 要在下限和上限 tick 之间存入流动性 ,必须存入与流动性为 的这些 tick 之间的段的实际储备相对应的 Token 数量。
  • 原文链接: rareskills.io/post/unisw...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论