一、factory

二、Liquidity

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);
}调用 _modifyPosition
- 根据当前价格和提供的区间,算出需要的 token0 / token1 数量。
- 更新 position 信息。
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data)
- 回调给调用方(通常是 Manager),让它把 token0/1 转进池子。
- 回调设计是 Uniswap 的一大特点,保证池子不用持有别人的授权。
校验资金到位。
触发 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。
如果 当前价格 < tickLower
→ 价格在区间左边
→ LP 的流动性还没生效(要等价格涨到区间才会生效)。
→ 用户只需要提供 token0。
如果 tickLower <= 当前价格 < tickUpper
→ 价格在区间内
→ 流动性马上生效。
→ 用户需要同时提供 token0 + token1。
如果 当前价格 >= 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() 才能真正提走代币。
核心逻辑
- 调用
_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 数量
- 取正数作为返回值
amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);- 这里 amount0Int、amount1Int 可能是负数,所以取反再转成正数。
- amount0 / amount1 = 本次 burn 这部分流动性应释放出来的 token0、token1 数量。
- 累加到 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 会真正打给你。
实际打钱时用的函数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); }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);
}
}
评论 (0)