Hello World

Uniswap v3-Part I

Uniswap v3

一、v3 与 v2 的差异

1. 流动性

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

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

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

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

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

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

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

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

4. TWAP 的计算方式

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

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

5. Uniswap V3 的优点和缺点

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

二、uniswap v3 架构

数学公式

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 算法

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

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

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

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

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;
2️⃣ 初始化交易缓存
SwapCache memory cache = {...};
SwapState memory state = {...};
3️⃣ 核心循环(逐 Tick 模拟)
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    StepComputations memory step;
    ...
    (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(...);

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

每一步(step):

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

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

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

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

循环结束后:

5️⃣ 计算最终的交易结果
(amount0, amount1) = zeroForOne == exactInput
    ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
    : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);

根据方向和模式算出:

6️⃣ 资金结算
if (zeroForOne) {
    if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
    ...
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
} else {
    ...
}
7️⃣ 事件 & 解锁
emit Swap(...);
slot0.unlocked = true;

Router 提供的 swap接口

函数调用链

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);
    }
}

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »