移除流动性指南

概述

移除流动性是将LP代币兑换回原始代币对的过程。流动性提供者可以随时移除部分或全部流动性,获得相应比例的代币以及累积的交易费用收益。

移除流动性基础

LP代币赎回机制

当您移除流动性时:

  1. 销毁相应数量的LP代币

  2. 按比例获得池子中的两种代币

  3. 获得累积的交易费用收益

  4. 可能面临无常损失

移除流动性的时机

考虑移除流动性的情况:

  • 需要使用锁定的资金

  • 市场波动较大,担心无常损失

  • 发现更好的投资机会

  • 交易费用收益下降

移除流动性的方式

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: '继续持有' };
    }
};

总结

移除流动性是流动性管理的重要环节,本指南涵盖了:

  1. 基础概念: LP代币赎回机制、移除时机

  2. 移除方式: 标准移除、JU流动性移除

  3. Permit功能: 无需预先授权的移除方式

  4. 特殊代币: 转账费代币的处理

  5. 策略管理: 部分移除、定期移除

  6. 分析工具: 收益计算、无常损失分析

  7. 批量操作: 批量移除、紧急移除

  8. 最佳实践: 时机选择、Gas优化、风险管理

通过合理的流动性移除策略,用户可以最大化收益并有效控制风险。