Uniswap V3-Prat II

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

一、factory

megvvrhi.png

二、Liquidity

megvrm7k.png

Pool

mint

用户调用NonfungiblePositionManager 的 mint 来调用 Pool 的 mint,获取到如果要增加 amount数量的流动性到 pool需要添加多少的 token0 和 token1

function mint(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount,
        bytes calldata data
    ) external override lock returns (uint256 amount0, uint256 amount1) {
        require(amount > 0);
        (, int256 amount0Int, int256 amount1Int) =
            _modifyPosition(
                ModifyPositionParams({
                    owner: recipient,
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    liquidityDelta: int256(amount).toInt128()
                })
            );

        amount0 = uint256(amount0Int);
        amount1 = uint256(amount1Int);

        uint256 balance0Before;
        uint256 balance1Before;
        if (amount0 > 0) balance0Before = balance0();
        if (amount1 > 0) balance1Before = balance1();
        IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
        if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
        if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');

        emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);
    }
  1. 调用 _modifyPosition
    • 根据当前价格和提供的区间,算出需要的 token0 / token1 数量。
    • 更新 position 信息。
  2. IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data)
    • 回调给调用方(通常是 Manager),让它把 token0/1 转进池子。
    • 回调设计是 Uniswap 的一大特点,保证池子不用持有别人的授权。
  3. 校验资金到位。
  4. 触发 Mint 事件。
function _modifyPosition(ModifyPositionParams memory params)
        private
        noDelegateCall
        returns (
            Position.Info storage position,
            int256 amount0,
            int256 amount1
        )
    {
        checkTicks(params.tickLower, params.tickUpper);

        Slot0 memory _slot0 = slot0; // SLOAD for gas optimization

        position = _updatePosition(
            params.owner,
            params.tickLower,
            params.tickUpper,
            params.liquidityDelta,
            _slot0.tick
        );

        if (params.liquidityDelta != 0) {
            if (_slot0.tick < params.tickLower) {
                // current tick is below the passed range; liquidity can only become in range by crossing from left to
                // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it
                amount0 = SqrtPriceMath.getAmount0Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
            } else if (_slot0.tick < params.tickUpper) {
                // current tick is inside the passed range
                uint128 liquidityBefore = liquidity; // SLOAD for gas optimization

                // write an oracle entry
                (slot0.observationIndex, slot0.observationCardinality) = observations.write(
                    _slot0.observationIndex,
                    _blockTimestamp(),
                    _slot0.tick,
                    liquidityBefore,
                    _slot0.observationCardinality,
                    _slot0.observationCardinalityNext
                );

                amount0 = SqrtPriceMath.getAmount0Delta(
                    _slot0.sqrtPriceX96,
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
                amount1 = SqrtPriceMath.getAmount1Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    _slot0.sqrtPriceX96,
                    params.liquidityDelta
                );

                liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
            } else {
                // current tick is above the passed range; liquidity can only become in range by crossing from right to
                // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it
                amount1 = SqrtPriceMath.getAmount1Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
            }
        }
    }
  • 核心逻辑:根据当前价格 slot0.tick 和 [tickLower, tickUpper] 的关系,决定需要多少 token0 和 token1。
  1. 如果 当前价格 < tickLower

    → 价格在区间左边

    → LP 的流动性还没生效(要等价格涨到区间才会生效)。

    → 用户只需要提供 token0

  2. 如果 tickLower <= 当前价格 < tickUpper

    → 价格在区间内

    → 流动性马上生效。

    → 用户需要同时提供 token0 + token1

  3. 如果 当前价格 >= tickUpper

    → 价格在区间右边

    → 流动性还没生效(要等价格跌到区间才会生效)。

    → 用户只需要提供 token1

🔹 _updatePosition的主要流程

当用户调用 mint()(增加流动性)或者 burn()(减少流动性)时,都会进入 _updatePosition,它要做的事可以总结为三步:

1.取出当前 position 信息

position = positions.get(owner, tickLower, tickUpper);

每个 LP 的区间 [tickLower, tickUpper] 有独立的 position,记录了流动性和手续费信息。

2.更新边界 tick(tickLower 和 tickUpper)

flippedLower = ticks.update(..., false, ...);
flippedUpper = ticks.update(..., true, ...);
  • tick.update 会调整每个 tick 的流动性(gross 和 net),并判断这个 tick 是否被“翻转”了(从无流动性变成有流动性,或者相反)。
  • 如果翻转了,tickBitmap 会 flip,表示这个 tick 现在是“活跃的边界”,swap 的时候可能会跨到它。

3.更新 position 里存储的 feeGrowth 累计值

(feeGrowthInside0X128, feeGrowthInside1X128) =
    ticks.getFeeGrowthInside(...);
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
  • 这一步是为了让 LP 将来能精确领取手续费(因为手续费是按“进入 position 的时刻”的增长率开始算的)。
🔹 ticks.update 内部逻辑

tick 本质上就是“流动性的边界点”,每个 tick 记录了 跨过它时要加/减多少流动性

uint128 liquidityGrossBefore = info.liquidityGross;
uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);
  • liquidityGross:这个 tick 上总共有多少流动性挂靠。
  • liquidityNet:当价格跨过这个 tick时,池子总流动性要加还是减。

再结合 upper 参数:

  • 如果是 下边界(lower tick):进入区间时要加 liquidity,出去时要减。
  • 如果是 上边界(upper tick):进入区间时要减 liquidity,出去时要加。

    所以 liquidityNet 更新时符号是反的:

info.liquidityNet = upper
    ? int256(info.liquidityNet).sub(liquidityDelta).toInt128()
    : int256(info.liquidityNet).add(liquidityDelta).toInt128();

burn

 function burn(
        int24 tickLower,
        int24 tickUpper,
        uint128 amount
    ) external override lock returns (uint256 amount0, uint256 amount1) {
        (Position.Info storage position, int256 amount0Int, int256 amount1Int) =
            _modifyPosition(
                ModifyPositionParams({
                    owner: msg.sender,
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    liquidityDelta: -int256(amount).toInt128()
                })
            );

        amount0 = uint256(-amount0Int);
        amount1 = uint256(-amount1Int);

        if (amount0 > 0 || amount1 > 0) {
            (position.tokensOwed0, position.tokensOwed1) = (
                position.tokensOwed0 + uint128(amount0),
                position.tokensOwed1 + uint128(amount1)
            );
        }

        emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);
    }
  • 作用:用户调用 NonfungiblePositionManager 的 burn 来调用 Pool 的 burn 从一个指定的 price range [tickLower, tickUpper] 中移除流动性
  • 输入参数

    • tickLower:这个 position 的下边界 tick
    • tickUpper:这个 position 的上边界 tick
    • amount:要移除多少流动性 (liquidity)
  • 返回值

    • amount0:用户从这次 burn 对应的 position 上应得的 token0 数量
    • amount1:用户从这次 burn 对应的 position 上应得的 token1 数量

⚠️ 注意:调用 burn 后,用户 并不会立刻拿到 token0 / token1,而是把它们记录到 position 的 tokensOwed0 / tokensOwed1 中。用户要再调用 collect() 才能真正提走代币。

核心逻辑
  1. 调用

_modifyPosition

(Position.Info storage position, int256 amount0Int, int256 amount1Int) =
    _modifyPosition(
        ModifyPositionParams({
            owner: msg.sender,
            tickLower: tickLower,
            tickUpper: tickUpper,
            liquidityDelta: -int256(amount).toInt128()
        })
    );
  • _modifyPosition 是内部函数,负责更新 position 的状态。
  • liquidityDelta 设置为 负数,表示减少流动性。
  • 它会返回:

    • position:用户在这个区间内的 position 结构体
    • amount0Int:对应减少这部分流动性时,position 内减少的 token0 数量(可能是负数)
    • amount1Int:对应减少这部分流动性时,position 内减少的 token1 数量
  1. 取正数作为返回值
amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);
  • 这里 amount0Int、amount1Int 可能是负数,所以取反再转成正数。
  • amount0 / amount1 = 本次 burn 这部分流动性应释放出来的 token0、token1 数量。
  1. 累加到 position 的应收款项
if (amount0 > 0 || amount1 > 0) {
    (position.tokensOwed0, position.tokensOwed1) = (
        position.tokensOwed0 + uint128(amount0),
        position.tokensOwed1 + uint128(amount1)
    );
}
  • 用户移除了流动性,但 Uniswap 不会马上转钱给你,而是把应得的 token0/token1 加到 tokensOwed0/tokensOwed1。
  • 等你再调用 collect() 时,这些累计的 owed token 会真正打给你。
  1. 实际打钱时用的函数collect

    function collect(
            address recipient,
            int24 tickLower,
            int24 tickUpper,
            uint128 amount0Requested,
            uint128 amount1Requested
        ) external override lock returns (uint128 amount0, uint128 amount1) {
            // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1}
            Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);
    
            amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
            amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;
    
            if (amount0 > 0) {
                position.tokensOwed0 -= amount0;
                TransferHelper.safeTransfer(token0, recipient, amount0);
            }
            if (amount1 > 0) {
                position.tokensOwed1 -= amount1;
                TransferHelper.safeTransfer(token1, recipient, amount1);
            }
    
            emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
        }
  2. fork 主网测试增删流动性接口

// 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 {INonfungiblePositionManager} from "../src/interfaces/uniswap-v3/INonfungiblePositionManager.sol";
import {UNISWAP_V3_NONFUNGIBLE_POSITION_MANAGER, DAI, WETH} from "../src/Constants.sol";

struct Position {
    uint96 nonce;
    address operator;
    address token0;
    address token1;
    uint24 fee;
    int24 tickLower;
    int24 tickUpper;
    uint128 liquidity;
    uint256 feeGrowthInside0LastX128;
    uint256 feeGrowthInside1LastX128;
    uint128 tokensOwed0;
    uint128 tokensOwed1;
}

contract UniswapV3LiquidityTest is Test {
    IWETH private constant weth = IWETH(WETH);
    IERC20 private constant dai = IERC20(DAI);
    INonfungiblePositionManager private constant manager =
        INonfungiblePositionManager(UNISWAP_V3_NONFUNGIBLE_POSITION_MANAGER);

    // 0.3%
    int24 private constant MIN_TICK = -887272;
    int24 private constant MAX_TICK = 887272;
    // DAI/WETH 3000
    uint24 private constant POOL_FEE = 3000;
    int24 private constant TICK_SPACING = 60;

    function setUp() public {
        deal(DAI, address(this), 3000 * 1e18);
        deal(WETH, address(this), 3 * 1e18);

        weth.approve(address(manager), type(uint256).max);
        dai.approve(address(manager), type(uint256).max);
    }

    function mint() private returns (uint256 tokenId) {
        (tokenId,,,) = manager.mint(
            INonfungiblePositionManager.MintParams({
                token0: DAI,
                token1: WETH,
                fee: POOL_FEE,
                tickLower: MIN_TICK / TICK_SPACING * TICK_SPACING,
                tickUpper: MAX_TICK / TICK_SPACING * TICK_SPACING,
                amount0Desired: 1000 * 1e18,
                amount1Desired: 1e18,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            })
        );
    }

    function getPosition(uint256 tokenId) private view returns (Position memory) {
        (
            uint96 nonce,
            address operator,
            address token0,
            address token1,
            uint24 fee,
            int24 tickLower,
            int24 tickUpper,
            uint128 liquidity,
            uint256 feeGrowthInside0LastX128,
            uint256 feeGrowthInside1LastX128,
            uint128 tokensOwed0,
            uint128 tokensOwed1
        ) = manager.positions(tokenId);

        Position memory position = Position({
            nonce: nonce,
            operator: operator,
            token0: token0,
            token1: token1,
            fee: fee,
            tickLower: tickLower,
            tickUpper: tickUpper,
            liquidity: liquidity,
            feeGrowthInside0LastX128: feeGrowthInside0LastX128,
            feeGrowthInside1LastX128: feeGrowthInside1LastX128,
            tokensOwed0: tokensOwed0,
            tokensOwed1: tokensOwed1
        });

        return position;
    }

    // Mint a new position by adding liquidity to DAI/WETH pool with 0.3% fee.
    // - You are free to choose the price range
    // - Ticks must be divisible by tick spacing of the pool
    // - This test contract is given 3000 DAI and 3 WETH. Put any amount of tokens
    //   not exceeding this contracts's balance.
    // - Set recipient of NFT (that represents the ownership of this position) to this contract.
    function test_mint() public {
        (uint256 tokenId,, uint256 amount0, uint256 amount1) = manager.mint(
            INonfungiblePositionManager.MintParams({
                token0: DAI,
                token1: WETH,
                fee: POOL_FEE,
                tickLower: MIN_TICK / TICK_SPACING * TICK_SPACING,
                tickUpper: MAX_TICK / TICK_SPACING * TICK_SPACING,
                amount0Desired: 1000 * 1e18,
                amount1Desired: 1e18,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            })
        );

        console2.log("Amount 0 added %e", amount0);
        console2.log("Amount 1 added %e", amount1);

        assertEq(manager.ownerOf(tokenId), address(this));

        Position memory position = getPosition(tokenId);
        assertEq(position.token0, DAI);
        assertEq(position.token1, WETH);
        assertGt(position.liquidity, 0);
    }

    // Increase liquidity for the position with token id = `tokenId`.
    // 3000 DAI and 3 WETH were initially given to this contract.
    // Some of the tokens where used to mint a new position.
    // Use any token amount less than or equal to contract's balance.
    function test_increaseLiquidity() public {
        uint256 tokenId = mint();
        Position memory p0 = getPosition(tokenId);

        (uint256 liquidityDelta, uint256 amount0, uint256 amount1) = manager.increaseLiquidity(
            INonfungiblePositionManager.IncreaseLiquidityParams({
                tokenId: tokenId,
                amount0Desired: 1000 * 1e18,
                amount1Desired: 1e18,
                amount0Min: 0,
                amount1Min: 0,
                deadline: block.timestamp
            })
        );

        console2.log("Amount 0 added %e", amount0);
        console2.log("Amount 1 added %e", amount1);

        Position memory p1 = getPosition(tokenId);
        assertGt(p1.liquidity, p0.liquidity);
        assertGt(liquidityDelta, 0);
    }

    // Decrease liquidity for the position with token id = `tokenId`.
    // - Amount of liquidity to decrease cannot exceed the position's liquidity.
    function test_decreaseLiquidity() public {
        uint256 tokenId = mint();
        Position memory p0 = getPosition(tokenId);

        (uint256 amount0, uint256 amount1) = manager.decreaseLiquidity(
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: p0.liquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline: block.timestamp
            })
        );

        console2.log("Amount 0 decreased %e", amount0);
        console2.log("Amount 1 decreased %e", amount1);

        Position memory p1 = getPosition(tokenId);
        assertEq(p1.liquidity, 0);
        assertGt(p1.tokensOwed0, 0);
        assertGt(p1.tokensOwed1, 0);
    }

    // Remove all liquidity (including fees) from a position by calling collect()
    // - Decrease all liquidity for the position with token id = `tokenId`
    // - Transfer tokens from NonFungiblePositionManager to this contract
    //   by calling collect()
    function test_collect() public {
        uint256 tokenId = mint();
        Position memory p0 = getPosition(tokenId);

        manager.decreaseLiquidity(
            INonfungiblePositionManager.DecreaseLiquidityParams({
                tokenId: tokenId,
                liquidity: p0.liquidity,
                amount0Min: 0,
                amount1Min: 0,
                deadline: block.timestamp
            })
        );
        (uint256 amount0, uint256 amount1) = manager.collect(
            INonfungiblePositionManager.CollectParams({
                tokenId: tokenId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            })
        );

        console2.log("--- collect ---");
        console2.log("Amount 0 collected %e", amount0);
        console2.log("Amount 1 collected %e", amount1);

        Position memory p1 = getPosition(tokenId);

        assertEq(p1.liquidity, 0);
        assertEq(p1.tokensOwed0, 0);
        assertEq(p1.tokensOwed1, 0);
    }
}
1

评论 (0)

取消