Uniswap v3-Part I

jerichou
2025-08-16 / 0 评论 / 7 阅读 / 正在检测是否收录...

Uniswap v3

一、v3 与 v2 的差异

1. 流动性

Uniswap V2 和 V3 在流动性提供和管理方面有根本性的差异,主要体现在流动性的分布和效率上。

  • Uniswap V2 的流动性:在 V2 中,流动性是均匀分布在整个价格曲线上(从 0 到无穷大)。流动性提供者(LP)将资金存入池子后,这些资金会根据恒定乘积公式(x * y = k)在所有可能的价格范围内可用。这意味着,无论当前价格如何,流动性都是“被动”分布的,导致资金利用率较低。因为在实际交易中,大部分交易发生在有限的价格范围内(如稳定币对的1%波动内),V2 的设计会造成大量资金闲置在极端价格区间,无法产生手续费收入。这使得 LP 的资本效率较低,容易遭受无常损失(Impermanent Loss),因为价格波动时,LP 的持仓会自动调整以维持 k 值不变。
  • Uniswap V3 的流动性:V3 引入了“集中流动性”(Concentrated Liquidity)的概念,允许 LP 在特定价格范围内提供流动性,而不是整个曲线。这类似于限价订单,LP 可以选择价格区间(如在当前价格的 ±10% 内),从而将资金集中在高交易量的区域,提高资本效率。流动性被分割成“位置”(Positions),每个位置定义了一个价格范围 [a, b],在该范围内资金才被使用。超出范围的流动性会闲置,但 LP 可以主动调整位置以跟随市场价格。这种设计大大提高了资金利用率,据官方数据,V3 的资本效率可比 V2 高出 4000 倍以上(取决于范围选择)。然而,这也增加了复杂性,因为 LP 需要监控和调整位置以避免资金闲置或过度暴露于无常损失。

总体差异:V2 的流动性更简单但效率低,适合被动投资者;V3 的流动性更灵活但需要主动管理,适合专业 LP。

2. V3 主动管理流动性基于 ERC721,V2 被动管理流动性基于 ERC20

这一方面直接延伸了流动性的差异,焦点在于流动性代币的标准和管理方式。

  • Uniswap V2 的被动管理流动性基于 ERC20:在 V2 中,流动性由 ERC20 标准的 LP 代币表示。这些代币是同质化的(Fungible),每个池子的 LP 代币代表对池子总流动性的比例份额。LP 提供流动性后获得这些代币,可以自由转让、交易或用于其他 DeFi 协议(如抵押借贷)。管理是“被动”的:一旦提供流动性,资金就会自动根据 AMM 公式调整,无需 LP 干预。移除流动性时,按比例返还代币。这种设计简单,适合初学者,但缺乏灵活性,因为 LP 无法控制资金在价格曲线上的分布。
  • Uniswap V3 的主动管理流动性基于 ERC721:V3 使用 ERC721 标准的 NFT(Non-Fungible Token)来表示流动性位置。每个位置都是独特的 NFT,包含特定价格范围、金额和所有者信息。这使得流动性是非同质化的,每个 NFT 代表一个自定义的价格区间。管理是“主动”的:LP 必须选择价格范围、监控市场,并定期调整位置(例如,通过重新定位 NFT 来跟随价格变动)。NFT 的设计允许更精细的控制,如合并多个位置或在二级市场交易 NFT 位置。这种基于 NFT 的方法提高了定制化,但也增加了 gas 费用和复杂性,因为每个位置都需要单独管理。

差异总结:V2 的 ERC20 适合被动、标准化管理,易于集成;V3 的 ERC721 强调主动、个性化管理,更像投资策略,但门槛更高。

3. 手续费的计算:V2 是固定 0.3%,而 V3 有四种计算方式

手续费结构是 Uniswap 演进的核心变化,影响了 LP 收入和交易者成本。

  • Uniswap V2 的手续费计算:V2 使用固定的 0.3% 手续费率,对每笔交易收取(例如,1000 USDC 交易收取 3 USDC)。这笔费用全部归 LP,所有池子统一费率,无需选择。简单明了,但不灵活,无法适应不同资产的波动性(如稳定币对需要低费率,波动币对需要高费率)。
  • Uniswap V3 的手续费计算:V3 引入了多层级费率,允许池子创建者选择适合的费率。目前有四种费率选项:0.01%(超低费率,适合稳定币对,如 USDC/USDT)、0.05%(低费率,适合低波动对)、0.30%(标准费率,与 V2 类似,适合大多数对)、1.00%(高费率,适合高波动或 exotic 资产)。费率在池子创建时固定,不能更改。手续费计算基于交易金额乘以费率,但只在 LP 的价格范围内收取(超出范围不产生费用)。此外,V3 引入了“协议费”(Protocol Fee),允许 Uniswap 治理从 LP 费用中抽取一部分(目前为 0,但可通过治理激活)。这四种方式让池子更适应市场,但也导致流动性碎片化(同一对可能有多个费率池子)。

差异:V2 的固定费率简单统一;V3 的多费率更市场化,但增加了选择复杂性。

4. TWAP 的计算方式

TWAP(Time-Weighted Average Price,时间加权平均价格)是 Uniswap 的 oracle 功能,用于提供可靠的价格数据,防止操纵。

  • Uniswap V2 的 TWAP 计算:V2 使用累积价格(Cumulative Price)机制。每个池子维护两个累积变量:price0CumulativeLast 和 price1CumulativeLast,分别记录 token0 和 token1 的时间加权价格(价格 * 时间增量)。TWAP 通过 (当前累积价格 - 上次累积价格) / 时间间隔 计算得出,支持查询任意时间段的平均价格。但 V2 的 oracle 较基础,容易受短期操纵影响(因为依赖最后区块的价格),且计算 gas 消耗较高。外部合约需手动计算 TWAP,通常需要至少 1 小时间隔以防操纵。
  • Uniswap V3 的 TWAP 计算:V3 显著改进了 oracle,引入了“观察数组”(Observations Array),一个固定大小(默认 500)的环形缓冲区,存储历史累积价格和时间戳。TWAP 使用二分查找从数组中插值计算,支持更精确的短期 TWAP(低至 9 秒)。计算公式类似 V2,但更高效:TWAP = (累积价格差 / 时间差),并使用几何平均来处理价格。V3 的 pair 合约内置 observe 函数,直接返回指定时间点的 TWAP 数组,减少了 gas 消耗并提高了抗操纵性(因为历史数据不可篡改)。此外,V3 支持“虚拟观察”(Virtual Observations),允许在不更新数组时估算当前 TWAP。

差异:V2 的 TWAP 简单但易操纵、gas 高;V3 的 TWAP 更精确、抗操纵强、效率高,适合 DeFi 集成如借贷协议。

5. Uniswap V3 的优点和缺点

Uniswap V3 相对于 V2 的改进带来了显著优势,但也引入了新挑战。

  • 优点

    • 更高的资本效率:集中流动性允许 LP 用更少的资金提供等效深度,潜在收益更高(手续费收入可增加数倍)。
    • 灵活性增强:多费率和自定义价格范围让 LP 像做市商一样操作,适应不同市场条件。
    • 更好的 oracle:改进的 TWAP 提供更可靠的价格数据,支持更多 DeFi 用例如衍生品。
    • gas 优化:尽管复杂,V3 的核心交易 gas 费用与 V2 相当,甚至在某些场景更低。
    • 创新潜力:NFT 位置启用新功能,如流动性挖矿激励针对特定范围,或位置的金融化(借贷 NFT)。
  • 缺点

    • 复杂性增加:主动管理要求 LP 具备市场知识,否则可能遭受更高无常损失或资金闲置。初学者容易出错。
    • 流动性碎片化:多费率池子导致同一交易对的流动性分散,可能增加滑点。
    • 更高风险:价格范围选择不当可能导致位置“出界”(Out-of-Range),资金不产生收入;此外,MEV(矿工可提取价值)攻击更常见。
    • gas 费用波动:调整位置或创建新池子 gas 更高,适合大额 LP。
    • 采用门槛:V3 的 NFT 和范围管理让集成更难,部分用户仍偏好 V2 的简单性。

二、uniswap v3 架构

medjty3r.png

数学公式

medjuq31.png

medjvkhp.png

medjw1le.png

fork 主网测试计算价格

pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IUniswapV3Pool} from "../src/interfaces/uniswap-v3/IUniswapV3Pool.sol";
import {UNISWAP_V3_POOL_USDC_WETH_500} from "../src/Constants.sol";
import {FullMath} from "../src/uniswap-v3/FullMath.sol";

contract UniswapV3SwapTest is Test {
    // token0 (X)
    uint256 private constant USDC_DECIMALS = 1e6;
    // token1 (Y)
    uint256 private constant WETH_DECIMALS = 1e18;
    // 1 << 96 = 2 ** 96
    uint256 private constant Q96 = 1 << 96;
    IUniswapV3Pool private immutable pool = IUniswapV3Pool(UNISWAP_V3_POOL_USDC_WETH_500);

    // - Get price of WETH in terms of USDC and return price with 18 decimals
    function test_spot_price_from_sqrtPriceX96() public {
        uint256 price = 0;
        IUniswapV3Pool.Slot0 memory slot0 = pool.slot0();

        // sqrtPriceX96 * sqrtPriceX96 might overflow
        // So use FullMath.mulDiv to do uint256 * uint256 / uint256 without overflow
        // price = FullMath.mulDiv(slot0.sqrtPriceX96, slot0.sqrtPriceX96, Q96);
        price = FullMath.mulDiv(slot0.sqrtPriceX96, slot0.sqrtPriceX96, Q96);
        // 1 / price = 1 / (P * Q96)
        price = 1e12 * 1e18 * Q96 / price;
        assertGt(price, 0, "price = 0");
        console2.log("price %e", price);
    }
}

三、Swap

Swap 算法

medjx58m.png

medjxktv.png

Ticks 是算法自动划分的数学网格,覆盖从负无穷到正无穷的价格范围(实际存储有边界)

在 tickLower 和 tickUpper 区间内,流动性为 L,当价格 p 朝着 ticklower 移动时,移动出 ticklower 后,最新的流动性等于当前流动性减去▲L,如果朝着 tickupper 移动时,移动出 tickupper 后,最新的流动性等于当前流动性加上▲L。

当价格从 ticklower 左侧移动到右侧时最新的流动性等于当前流动性加上▲L,如果价格从 tickupper 右侧移动到左侧时最新的流动性等于当前流动性减去▲L。

medjy2uz.png

同理可以推导出增减 y 时的价格

medjyird.png

medjz764.png

Swap 实现

swap 函数是 Uniswap V3 核心流动性池的核心交易实现,它就是在链上把 “用户输入一定 token,沿着价格曲线撮合流动性,跨 tick 调整价格,收取手续费,更新全局状态

1️⃣ 前置检查 & 交易方向
require(amountSpecified != 0, 'AS');
...
require(
    zeroForOne
        ? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO
        : sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO,
    'SPL'
);
slot0.unlocked = false;
  • amountSpecified > 0 → Exact In 模式
  • amountSpecified < 0 → Exact Out 模式
  • zeroForOne 表示交易方向(token0 → token1)
  • 检查价格限制 sqrtPriceLimitX96 是否合理,防止价格滑到极端值
  • 锁池(防重入)
2️⃣ 初始化交易缓存
SwapCache memory cache = {...};
SwapState memory state = {...};
  • SwapCache 保存本次交易不变的信息(初始流动性、协议费参数、当前时间等)
  • SwapState 是动态状态(剩余的 amount、当前价格、tick、全局手续费累积、流动性等)
3️⃣ 核心循环(逐 Tick 模拟)
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    StepComputations memory step;
    ...
    (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(...);

这个循环就是 沿着价格曲线一步步执行 swap,直到:

  • 用完用户的输入(Exact In)或
  • 达到目标输出(Exact Out)或
  • 触及价格限制

每一步(step):

  1. 找到下一个已初始化的 tick (tickNext)
  2. 计算从当前价格走到下一个 tick 或价格限制需要的量
  3. 调用 SwapMath.computeSwapStep:

    • 根据恒定乘积公式 + 手续费算出:

      • amountIn(净输入)
      • amountOut(输出)
      • feeAmount(手续费)
      • sqrtPriceX96(新的价格)
  4. 更新剩余需求(amountSpecifiedRemaining)
  5. 更新 feeGrowthGlobalX128(全局手续费累积)
  6. 如果跨过一个已初始化的 tick:

    • 触发 ticks.cross,调整流动性(增加或减少)
    • 更新当前 tick
4️⃣ 更新价格 & 流动性状态

循环结束后:

  • 如果 tick 变了,更新 slot0(价格、tick、oracle 观测点)
  • 更新全局流动性 liquidity(如果跨 tick 流动性发生变化)
  • 更新全局手续费计数器 & 协议费记录
5️⃣ 计算最终的交易结果
(amount0, amount1) = zeroForOne == exactInput
    ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
    : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);

根据方向和模式算出:

  • 用户实际输入了多少 token0/token1
  • 用户实际收到了多少 token0/token1
6️⃣ 资金结算
if (zeroForOne) {
    if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
    ...
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
} else {
    ...
}
  • 如果用户要收的币(amountOut)是正数,就直接 safeTransfer 给用户
  • 调用 uniswapV3SwapCallback 让调用方支付输入币
  • 检查支付是否足额(余额变化)
7️⃣ 事件 & 解锁
emit Swap(...);
slot0.unlocked = true;
  • 发出 Swap 事件(链上日志)
  • 解锁池子

Router 提供的 swap接口

medk9axy.png

函数调用链

medk0aab.png

medk0qcq.png

fork 主网测试 swap 的接口

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {IWETH} from "../src/interfaces/IWETH.sol";
import {ISwapRouter} from "../src/interfaces/uniswap-v3/ISwapRouter.sol";
import {DAI, WETH, WBTC, UNISWAP_V3_SWAP_ROUTER_02} from "../src/Constants.sol";

contract UniswapV3SwapTest is Test {
    IWETH private weth = IWETH(WETH);
    IERC20 private dai = IERC20(DAI);
    IERC20 private wbtc = IERC20(WBTC);
    ISwapRouter private router = ISwapRouter(UNISWAP_V3_SWAP_ROUTER_02);
    uint24 private constant POOL_FEE = 3000;

    function setUp() public {
        deal(DAI, address(this), 1000 * 1e18);
        dai.approve(address(router), type(uint256).max);
    }

    // - Swap 1000 DAI for WETH on DAI/WETH pool with 0.3% fee
    // - Send WETH from Uniswap V3 to this contract
    function test_exactInputSingle() public {
        uint256 wethBefore = weth.balanceOf(address(this));

        // Call router.exactInputSingle
        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
            tokenIn: DAI,
            tokenOut: WETH,
            fee: POOL_FEE,
            recipient: address(this),
            amountIn: 1000 * 1e18,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        });

        uint256 amountOut = router.exactInputSingle(params);
        uint256 wethAfter = weth.balanceOf(address(this));

        console2.log("WETH amount out %e", amountOut);
        assertGt(amountOut, 0);
        assertEq(wethAfter - wethBefore, amountOut);
    }

    // Swap 1000 DAI for WETH and then WETH to WBTC
    // - Swap DAI to WETH on pool with 0.3% fee
    // - Swap WETH to WBTC on pool with 0.3% fee
    // - Send WBTC from Uniswap V3 to this contract
    // NOTE: WBTC has 8 decimals
    function test_exactInput() public {
        // Call router.exactInput
        // DAI->WETH->WBTC
        bytes memory path = abi.encodePacked(DAI, POOL_FEE, WETH, POOL_FEE, WBTC);
        ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({
            path: path,
            recipient: address(this),
            amountIn: 1000 * 1e18,
            amountOutMinimum: 0
        });
        uint256 amountOut = router.exactInput(params);

        console2.log("WBTC amount out %e", amountOut);
        assertGt(amountOut, 0);
        assertEq(wbtc.balanceOf(address(this)), amountOut);
    }

    // - Swap maximum of 1000 DAI to obtain exactly 0.1 WETH from DAI/WETH pool with 0.3% fee
    // - Send WETH from Uniswap V3 to this contract
    function test_exactOutputSingle() public {
        uint256 wethBefore = weth.balanceOf(address(this));

        ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({
            tokenIn: DAI,
            tokenOut: WETH,
            fee: POOL_FEE,
            recipient: address(this),
            amountOut: 0.1 * 1e18,
            amountInMaximum: 1000 * 1e18,
            sqrtPriceLimitX96: 0
        });
        uint256 amountIn = router.exactOutputSingle(params);

        uint256 wethAfter = weth.balanceOf(address(this));

        console2.log("DAI amount in %e", amountIn);
        assertLe(amountIn, 1000 * 1e18);
        assertEq(wethAfter - wethBefore, 0.1 * 1e18);
    }

    // Swap maximum of 1000 DAI to obtain exactly 0.01 WBTC
    // - Swap DAI to WETH on pool with 0.3% fee
    // - Swap WETH to WBTC on pool with 0.3% fee
    // - Send WBTC from Uniswap V3 to this contract
    // NOTE: WBTC has 8 decimals
    function test_exactOutput() public {
        // Call router.exactOutput
        bytes memory path = abi.encodePacked(WBTC, POOL_FEE, WETH, POOL_FEE, DAI);
        ISwapRouter.ExactOutputParams memory params = ISwapRouter.ExactOutputParams({
            path: path,
            recipient: address(this),
            amountOut: 0.01 * 1e18,
            amountInMaximum: 1000 * 1e18
        });
        uint256 amountIn = router.exactOutput(params);

        console2.log("DAI amount in %e", amountIn);
        assertLe(amountIn, 1000 * 1e18);
        assertEq(wbtc.balanceOf(address(this)), 0.01 * 1e8);
    }
}
1

评论 (0)

取消