移除流动性指南
概述
移除流动性是将LP代币兑换回原始代币对的过程。流动性提供者可以随时移除部分或全部流动性,获得相应比例的代币以及累积的交易费用收益。
移除流动性基础
LP代币赎回机制
当您移除流动性时:
销毁相应数量的LP代币
按比例获得池子中的两种代币
获得累积的交易费用收益
可能面临无常损失
移除流动性的时机
考虑移除流动性的情况:
需要使用锁定的资金
市场波动较大,担心无常损失
发现更好的投资机会
交易费用收益下降
移除流动性的方式
1. 标准移除流动性
移除代币对流动性,获得两种代币:
async function removeLiquidity(
tokenA,
tokenB,
fee,
liquidity,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
// 1. 获取交易对地址
const pairAddress = await factory.getPair(tokenA, tokenB, fee);
if (pairAddress === ethers.constants.AddressZero) {
throw new Error("交易对不存在");
}
// 2. 预估可获得的代币数量
const pair = new ethers.Contract(pairAddress, pairABI, provider);
const [reserves, totalSupply] = await Promise.all([
pair.getReserves(),
pair.totalSupply()
]);
const token0 = await pair.token0();
const [reserveA, reserveB] = tokenA.toLowerCase() === token0.toLowerCase()
? [reserves.reserve0, reserves.reserve1]
: [reserves.reserve1, reserves.reserve0];
const amountA = liquidity.mul(reserveA).div(totalSupply);
const amountB = liquidity.mul(reserveB).div(totalSupply);
// 3. 计算最小数量(滑点保护)
const slippageBps = Math.floor(slippagePercent * 100);
const amountAMin = amountA.mul(10000 - slippageBps).div(10000);
const amountBMin = amountB.mul(10000 - slippageBps).div(10000);
// 4. 授权LP代币
await ensureLPTokenApproval(pairAddress, ROUTER_ADDRESS, liquidity, signer);
// 5. 移除流动性
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.removeLiquidity(
tokenA,
tokenB,
fee,
liquidity,
amountAMin,
amountBMin,
userAddress,
deadline
);
console.log("移除流动性交易已提交:", tx.hash);
const receipt = await tx.wait();
return {
transactionHash: tx.hash,
blockNumber: receipt.blockNumber,
expectedAmountA: amountA,
expectedAmountB: amountB,
actualAmountA: await getActualAmountFromReceipt(receipt, 'amountA'),
actualAmountB: await getActualAmountFromReceipt(receipt, 'amountB')
};
}
// 辅助函数:确保LP代币授权
async function ensureLPTokenApproval(pairAddress, spenderAddress, amount, signer) {
const pair = new ethers.Contract(pairAddress, pairABI, signer);
const userAddress = await signer.getAddress();
const currentAllowance = await pair.allowance(userAddress, spenderAddress);
if (currentAllowance.lt(amount)) {
console.log("授权LP代币...");
const approveTx = await pair.approve(spenderAddress, amount);
await approveTx.wait();
console.log("LP代币授权完成");
}
}
2. 移除JU流动性
移除JU和ERC-20代币的流动性:
async function removeLiquidityETH(
token,
fee,
liquidity,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
// 1. 获取交易对信息
const pairAddress = await factory.getPair(token, WJU_ADDRESS, fee);
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// 2. 预估可获得的数量
const [reserves, totalSupply] = await Promise.all([
pair.getReserves(),
pair.totalSupply()
]);
const token0 = await pair.token0();
const [amountToken, amountETH] = token.toLowerCase() === token0.toLowerCase()
? [
liquidity.mul(reserves.reserve0).div(totalSupply),
liquidity.mul(reserves.reserve1).div(totalSupply)
]
: [
liquidity.mul(reserves.reserve1).div(totalSupply),
liquidity.mul(reserves.reserve0).div(totalSupply)
];
// 3. 计算最小数量
const slippageBps = Math.floor(slippagePercent * 100);
const amountTokenMin = amountToken.mul(10000 - slippageBps).div(10000);
const amountETHMin = amountETH.mul(10000 - slippageBps).div(10000);
// 4. 授权LP代币
await ensureLPTokenApproval(pairAddress, ROUTER_ADDRESS, liquidity, signer);
// 5. 移除流动性
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.removeLiquidityETH(
token,
fee,
liquidity,
amountTokenMin,
amountETHMin,
userAddress,
deadline
);
console.log("移除JU流动性交易已提交:", tx.hash);
return tx;
}
使用Permit移除流动性
无需预先授权的移除
使用EIP-2612 Permit功能,通过签名直接移除流动性:
async function removeLiquidityWithPermit(
tokenA,
tokenB,
fee,
liquidity,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
// 1. 获取交易对地址
const pairAddress = await factory.getPair(tokenA, tokenB, fee);
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// 2. 计算预期数量和最小数量
const [reserves, totalSupply] = await Promise.all([
pair.getReserves(),
pair.totalSupply()
]);
const token0 = await pair.token0();
const [reserveA, reserveB] = tokenA.toLowerCase() === token0.toLowerCase()
? [reserves.reserve0, reserves.reserve1]
: [reserves.reserve1, reserves.reserve0];
const amountA = liquidity.mul(reserveA).div(totalSupply);
const amountB = liquidity.mul(reserveB).div(totalSupply);
const slippageBps = Math.floor(slippagePercent * 100);
const amountAMin = amountA.mul(10000 - slippageBps).div(10000);
const amountBMin = amountB.mul(10000 - slippageBps).div(10000);
// 3. 生成Permit签名
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const permitSignature = await generatePermitSignature(
pairAddress,
userAddress,
ROUTER_ADDRESS,
liquidity,
deadline,
signer
);
// 4. 使用Permit移除流动性
const tx = await router.removeLiquidityWithPermit(
tokenA,
tokenB,
fee,
liquidity,
amountAMin,
amountBMin,
userAddress,
deadline,
false, // approveMax
permitSignature.v,
permitSignature.r,
permitSignature.s
);
console.log("Permit移除流动性交易已提交:", tx.hash);
return tx;
}
// 生成Permit签名
async function generatePermitSignature(
tokenAddress,
owner,
spender,
value,
deadline,
signer
) {
const token = new ethers.Contract(tokenAddress, pairABI, provider);
// 获取当前nonce
const nonce = await token.nonces(owner);
// 获取链ID
const chainId = await signer.getChainId();
// 构建EIP-712域
const domain = {
name: 'JAMM LPs',
version: '1',
chainId: chainId,
verifyingContract: tokenAddress
};
// 构建消息类型
const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
};
// 构建消息值
const message = {
owner: owner,
spender: spender,
value: value,
nonce: nonce,
deadline: deadline
};
// 生成签名
const signature = await signer._signTypedData(domain, types, message);
const { v, r, s } = ethers.utils.splitSignature(signature);
return { v, r, s, nonce, deadline };
}
JU流动性的Permit移除
async function removeLiquidityETHWithPermit(
token,
fee,
liquidity,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
const pairAddress = await factory.getPair(token, WJU_ADDRESS, fee);
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// 计算预期数量
const [reserves, totalSupply] = await Promise.all([
pair.getReserves(),
pair.totalSupply()
]);
const token0 = await pair.token0();
const [amountToken, amountETH] = token.toLowerCase() === token0.toLowerCase()
? [
liquidity.mul(reserves.reserve0).div(totalSupply),
liquidity.mul(reserves.reserve1).div(totalSupply)
]
: [
liquidity.mul(reserves.reserve1).div(totalSupply),
liquidity.mul(reserves.reserve0).div(totalSupply)
];
const slippageBps = Math.floor(slippagePercent * 100);
const amountTokenMin = amountToken.mul(10000 - slippageBps).div(10000);
const amountETHMin = amountETH.mul(10000 - slippageBps).div(10000);
// 生成Permit签名
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const permitSignature = await generatePermitSignature(
pairAddress,
userAddress,
ROUTER_ADDRESS,
liquidity,
deadline,
signer
);
const tx = await router.removeLiquidityETHWithPermit(
token,
fee,
liquidity,
amountTokenMin,
amountETHMin,
userAddress,
deadline,
false, // approveMax
permitSignature.v,
permitSignature.r,
permitSignature.s
);
return tx;
}
支持转账费代币的移除
转账费代币流动性移除
对于转账费代币,需要使用特殊的移除函数:
async function removeLiquidityETHSupportingFeeOnTransferTokens(
token,
fee,
liquidity,
slippagePercent,
userAddress,
signer
) {
const router = new ethers.Contract(ROUTER_ADDRESS, routerABI, signer);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
const pairAddress = await factory.getPair(token, WJU_ADDRESS, fee);
// 对于转账费代币,很难准确预估输出数量
// 设置较大的滑点容忍度
const adjustedSlippage = Math.max(slippagePercent, 5); // 至少5%滑点
// 授权LP代币
await ensureLPTokenApproval(pairAddress, ROUTER_ADDRESS, liquidity, signer);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
const tx = await router.removeLiquidityETHSupportingFeeOnTransferTokens(
token,
fee,
liquidity,
0, // 设置为0,因为难以准确预估
0, // 设置为0,因为难以准确预估
userAddress,
deadline
);
console.log("移除转账费代币流动性交易已提交:", tx.hash);
return tx;
}
部分移除策略
按百分比移除
async function removePartialLiquidity(
tokenA,
tokenB,
fee,
percentage, // 移除的百分比 (0-100)
userAddress,
signer
) {
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
// 1. 获取用户的LP代币余额
const pairAddress = await factory.getPair(tokenA, tokenB, fee);
const pair = new ethers.Contract(pairAddress, pairABI, provider);
const totalLPBalance = await pair.balanceOf(userAddress);
if (totalLPBalance.eq(0)) {
throw new Error("没有LP代币可移除");
}
// 2. 计算要移除的数量
const liquidityToRemove = totalLPBalance.mul(percentage * 100).div(10000);
console.log(`移除 ${percentage}% 的流动性`);
console.log(`总LP代币: ${ethers.utils.formatEther(totalLPBalance)}`);
console.log(`移除数量: ${ethers.utils.formatEther(liquidityToRemove)}`);
// 3. 执行移除
const result = await removeLiquidity(
tokenA,
tokenB,
fee,
liquidityToRemove,
1, // 1%滑点
userAddress,
signer
);
return result;
}
// 使用示例
await removePartialLiquidity(
TOKEN_A_ADDRESS,
TOKEN_B_ADDRESS,
100, // 1%费率
25, // 移除25%
userAddress,
signer
);
定期移除策略
class PeriodicLiquidityRemover {
constructor(routerAddress, signer) {
this.router = new ethers.Contract(routerAddress, routerABI, signer);
this.signer = signer;
this.schedules = [];
}
// 添加定期移除计划
addSchedule(pairInfo, percentage, intervalDays) {
this.schedules.push({
...pairInfo,
percentage,
intervalDays,
lastRemoval: 0,
nextRemoval: Date.now() + intervalDays * 24 * 60 * 60 * 1000
});
}
// 检查并执行到期的移除
async checkAndExecute() {
const now = Date.now();
for (const schedule of this.schedules) {
if (now >= schedule.nextRemoval) {
try {
console.log(`执行定期移除: ${schedule.tokenA}/${schedule.tokenB}`);
await removePartialLiquidity(
schedule.tokenA,
schedule.tokenB,
schedule.fee,
schedule.percentage,
await this.signer.getAddress(),
this.signer
);
// 更新下次移除时间
schedule.lastRemoval = now;
schedule.nextRemoval = now + schedule.intervalDays * 24 * 60 * 60 * 1000;
} catch (error) {
console.error("定期移除失败:", error.message);
}
}
}
}
// 启动定期检查
startScheduler(checkIntervalMinutes = 60) {
setInterval(() => {
this.checkAndExecute();
}, checkIntervalMinutes * 60 * 1000);
}
}
流动性分析工具
收益计算
async function calculateLiquidityReturns(
pairAddress,
userAddress,
addedAmount,
addedTimestamp
) {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// 获取当前LP余额
const currentLP = await pair.balanceOf(userAddress);
// 计算当前价值
const currentValue = await calculateLiquidityValue(pairAddress, currentLP, userAddress);
// 计算时间差
const timeHeld = (Date.now() / 1000) - addedTimestamp; // 秒
const daysHeld = timeHeld / (24 * 60 * 60);
// 计算收益率
const initialValue = addedAmount; // 假设初始价值
const currentTotalValue = currentValue.amount0.add(currentValue.amount1); // 简化计算
const returns = currentTotalValue.sub(initialValue);
const returnPercentage = returns.mul(10000).div(initialValue); // 基点
// 年化收益率
const annualizedReturn = returnPercentage.mul(365).div(Math.floor(daysHeld));
return {
initialValue: initialValue,
currentValue: currentTotalValue,
returns: returns,
returnPercentage: returnPercentage.toNumber() / 100, // 百分比
annualizedReturn: annualizedReturn.toNumber() / 100,
daysHeld: daysHeld,
currentLP: currentLP
};
}
无常损失计算
function calculateImpermanentLoss(
initialPriceRatio,
currentPriceRatio,
initialAmountA,
initialAmountB
) {
// 计算如果只是持有代币的价值
const holdValue = initialAmountA.add(
initialAmountB.mul(currentPriceRatio).div(initialPriceRatio)
);
// 计算LP的当前价值(简化计算)
const k = initialAmountA.mul(initialAmountB);
const newAmountA = sqrt(k.mul(initialPriceRatio).div(currentPriceRatio));
const newAmountB = k.div(newAmountA);
const lpValue = newAmountA.add(newAmountB.mul(currentPriceRatio).div(initialPriceRatio));
// 无常损失
const impermanentLoss = holdValue.sub(lpValue);
const impermanentLossPercentage = impermanentLoss.mul(10000).div(holdValue);
return {
holdValue: holdValue,
lpValue: lpValue,
impermanentLoss: impermanentLoss,
impermanentLossPercentage: impermanentLossPercentage.toNumber() / 100
};
}
// 简化的平方根函数
function sqrt(value) {
if (value.eq(0)) return ethers.BigNumber.from(0);
let z = value.add(1).div(2);
let y = value;
while (z.lt(y)) {
y = z;
z = value.div(z).add(z).div(2);
}
return y;
}
批量移除操作
批量移除多个流动性
async function batchRemoveLiquidity(removeParams, signer) {
const results = [];
for (const params of removeParams) {
try {
console.log(`移除流动性: ${params.tokenA}/${params.tokenB}`);
let result;
if (params.usePermit) {
result = await removeLiquidityWithPermit(
params.tokenA,
params.tokenB,
params.fee,
params.liquidity,
params.slippage || 1,
params.userAddress,
signer
);
} else {
result = await removeLiquidity(
params.tokenA,
params.tokenB,
params.fee,
params.liquidity,
params.slippage || 1,
params.userAddress,
signer
);
}
results.push({
success: true,
pair: `${params.tokenA}/${params.tokenB}`,
...result
});
// 等待避免nonce冲突
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
results.push({
success: false,
pair: `${params.tokenA}/${params.tokenB}`,
error: error.message
});
}
}
return results;
}
紧急移除所有流动性
async function emergencyRemoveAllLiquidity(userAddress, signer) {
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
// 获取所有交易对
const totalPairs = await factory.allPairsLength();
const userLiquidities = [];
// 检查用户在每个交易对中的流动性
for (let i = 0; i < totalPairs; i++) {
try {
const pairAddress = await factory.allPairs(i);
const pair = new ethers.Contract(pairAddress, pairABI, provider);
const balance = await pair.balanceOf(userAddress);
if (balance.gt(0)) {
const [token0, token1, fee] = await Promise.all([
pair.token0(),
pair.token1(),
pair.fee()
]);
userLiquidities.push({
tokenA: token0,
tokenB: token1,
fee: fee,
liquidity: balance,
pairAddress: pairAddress,
userAddress: userAddress
});
}
} catch (error) {
console.error(`检查交易对 ${i} 失败:`, error.message);
}
}
console.log(`发现 ${userLiquidities.length} 个流动性位置`);
// 批量移除所有流动性
if (userLiquidities.length > 0) {
const results = await batchRemoveLiquidity(userLiquidities, signer);
return results;
}
return [];
}
最佳实践
1. 移除时机选择
const removalTiming = {
// 基于市场条件
checkMarketConditions: (volatility, trend) => {
if (volatility > 0.5 && trend === 'BEARISH') {
return { action: 'REMOVE', reason: '高波动下跌市场,建议移除' };
} else if (volatility < 0.2 && trend === 'BULLISH') {
return { action: 'HOLD', reason: '低波动上涨市场,建议持有' };
}
return { action: 'MONITOR', reason: '继续观察市场' };
},
// 基于收益率
checkReturns: (currentReturn, targetReturn, timeHeld) => {
if (currentReturn >= targetReturn) {
return { action: 'REMOVE', reason: '达到目标收益率' };
} else if (currentReturn < -0.1 && timeHeld > 30) { // 亏损超过10%且持有超过30天
return { action: 'CONSIDER_REMOVE', reason: '持续亏损,考虑止损' };
}
return { action: 'HOLD', reason: '未达到移除条件' };
}
};
2. Gas优化策略
async function optimizeRemovalGas(removalParams, gasPrice) {
// 优先使用Permit以节省Gas
const permitCapable = await checkPermitSupport(removalParams.pairAddress);
if (permitCapable) {
console.log("使用Permit移除流动性以节省Gas");
return await removeLiquidityWithPermit(...removalParams);
}
// 批量授权以节省Gas
const approvals = removalParams.map(params =>
ensureLPTokenApproval(params.pairAddress, ROUTER_ADDRESS, params.liquidity, signer)
);
await Promise.all(approvals);
// 按Gas效率排序
const sortedParams = removalParams.sort((a, b) => {
// 优先处理大额流动性
return b.liquidity.gt(a.liquidity) ? 1 : -1;
});
return sortedParams;
}
async function checkPermitSupport(pairAddress) {
try {
const pair = new ethers.Contract(pairAddress, pairABI, provider);
await pair.DOMAIN_SEPARATOR();
return true;
} catch (error) {
return false;
}
}
3. 风险管理
const liquidityRiskManagement = {
// 设置止损点
setStopLoss: (initialValue, stopLossPercentage = 10) => {
return initialValue.mul(100 - stopLossPercentage).div(100);
},
// 设置止盈点
setTakeProfit: (initialValue, takeProfitPercentage = 50) => {
return initialValue.mul(100 + takeProfitPercentage).div(100);
},
// 检查是否需要调整
checkRebalance: (currentValue, stopLoss, takeProfit) => {
if (currentValue.lte(stopLoss)) {
return { action: 'STOP_LOSS', message: '触发止损' };
} else if (currentValue.gte(takeProfit)) {
return { action: 'TAKE_PROFIT', message: '触发止盈' };
}
return { action: 'HOLD', message: '继续持有' };
}
};
总结
移除流动性是流动性管理的重要环节,本指南涵盖了:
基础概念: LP代币赎回机制、移除时机
移除方式: 标准移除、JU流动性移除
Permit功能: 无需预先授权的移除方式
特殊代币: 转账费代币的处理
策略管理: 部分移除、定期移除
分析工具: 收益计算、无常损失分析
批量操作: 批量移除、紧急移除
最佳实践: 时机选择、Gas优化、风险管理
通过合理的流动性移除策略,用户可以最大化收益并有效控制风险。