跳转到主要内容

提供流动性 (V3)

V3 允许 LP 将流动性集中在特定价格范围内,以获得更高的资本效率。每个头寸都是一个具有唯一参数的 NFT。

创建头寸

1. 选择池参数

  • 代币对:两种代币
  • 手续费等级:0.01%、0.05%、0.3% 或 1%(参阅 手续费等级
  • 价格范围[tickLower, tickUpper](参阅 Ticks 与范围

2. 获取当前池状态

const V3_FACTORY = '0x0dfb1Bb755d872EA1fa4d95E4ad0c2E6317Ce9B9'; // 主网
const V3_POSITION_MANAGER = '0x343b244bEDF133D57C61b241557bF29AA32ea4F9';

const FACTORY_ABI = ['function getPool(address,address,uint24) view returns (address)'];
const POOL_ABI = [
  'function slot0() view returns (uint160, int24, uint16, uint16, uint16, uint8, bool)',
  'function tickSpacing() view returns (int24)'
];

const factory = new ethers.Contract(V3_FACTORY, FACTORY_ABI, provider);
const poolAddress = await factory.getPool(token0, token1, 3000);
const pool = new ethers.Contract(poolAddress, POOL_ABI, provider);

const [sqrtPriceX96, currentTick] = await pool.slot0();
const tickSpacing = await pool.tickSpacing();

3. 计算 Tick 范围

将你期望的价格范围对齐到池的 tick 间距:
function nearestUsableTick(tick, tickSpacing) {
  return Math.round(tick / tickSpacing) * tickSpacing;
}

// 示例:当前价格上下浮动 ±10%
const tickLower = nearestUsableTick(currentTick - 2000, tickSpacing);
const tickUpper = nearestUsableTick(currentTick + 2000, tickSpacing);

4. 授权代币

await token0Contract.approve(V3_POSITION_MANAGER, amount0Desired);
await token1Contract.approve(V3_POSITION_MANAGER, amount1Desired);

5. 铸造头寸

const PM_ABI = [
  'function mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256)) payable returns (uint256,uint128,uint256,uint256)'
];

const pm = new ethers.Contract(V3_POSITION_MANAGER, PM_ABI, signer);
const deadline = Math.floor(Date.now() / 1000) + 1200;

const tx = await pm.mint({
  token0,
  token1,
  fee: 3000,
  tickLower,
  tickUpper,
  amount0Desired,
  amount1Desired,
  amount0Min: amount0Desired * 99n / 100n,
  amount1Min: amount1Desired * 99n / 100n,
  recipient: userAddress,
  deadline
});

const receipt = await tx.wait();
// 从交易回执事件中解析 tokenId
token0 的地址必须小于 token1。调用前请先排序。

使用原生 KAS

如果其中一个代币是 KAS,将其作为 value 发送,而不是授权 WKAS:
const tx = await pm.mint(params, { value: kasAmount });

增加流动性

向现有头寸(相同 tick 范围)追加更多代币:
const tx = await pm.increaseLiquidity({
  tokenId,
  amount0Desired,
  amount1Desired,
  amount0Min: 0,
  amount1Min: 0,
  deadline
});

领取手续费

领取累积的交易手续费:
const tx = await pm.collect({
  tokenId,
  recipient: userAddress,
  amount0Max: ethers.MaxUint128,
  amount1Max: ethers.MaxUint128
});

移除流动性

1. 减少流动性

const tx = await pm.decreaseLiquidity({
  tokenId,
  liquidity: liquidityToRemove,  // 完全移除时使用全部流动性
  amount0Min: 0,
  amount1Min: 0,
  deadline
});

2. 提取代币

减少流动性后,代币由 Position Manager 持有。调用 collect() 来提取:
const tx = await pm.collect({
  tokenId,
  recipient: userAddress,
  amount0Max: ethers.MaxUint128,
  amount1Max: ethers.MaxUint128
});

3. 销毁 NFT(可选)

移除所有流动性并领取所有代币/手续费后:
const tx = await pm.burn(tokenId);

单边头寸

你可以创建完全在当前价格之上或之下的头寸:
范围存入行为
高于当前价格(tickLower > currentTick仅 Token0相当于限价卖出订单
低于当前价格(tickUpper < currentTick仅 Token1相当于限价买入订单
// 限价卖出:仅提供 token0,范围在当前价格之上
const tx = await pm.mint({
  token0,
  token1,
  fee: 3000,
  tickLower: currentTick + 60,    // 高于当前价格
  tickUpper: currentTick + 600,
  amount0Desired: sellAmount,
  amount1Desired: 0,              // 不需要 token1
  amount0Min: 0,
  amount1Min: 0,
  recipient: userAddress,
  deadline
});

要点

  • 每个头寸都是一个 NFT — 跟踪 tokenId 以进行后续操作
  • 手续费必须主动领取(不会自动复利)
  • 超出范围的头寸不会赚取手续费,直到价格重新进入范围
  • 范围越窄 = 资本效率越高,但需要更积极的管理
  • 使用 Position Manager 上的 multicall() 可以在一笔交易中批量执行减少流动性 + 领取