Removing Liquidity

Overview

Removing liquidity is the process of exchanging LP tokens back into the original pair of tokens. Liquidity providers can remove some or all of their liquidity at any time to receive a proportional share of the tokens and the accumulated trading fee earnings.

Basics of Removing Liquidity

LP Token Redemption Mechanism

When you remove liquidity:

  1. A corresponding amount of LP tokens is burned.

  2. You receive a proportional share of the two tokens in the pool.

  3. You receive the accumulated trading fee earnings.

  4. You may face impermanent loss.

When to Remove Liquidity

Consider removing liquidity in the following situations:

  • You need to use the locked funds.

  • The market is highly volatile, and you are concerned about impermanent loss.

  • You have found a better investment opportunity.

  • The trading fee earnings have decreased.

Methods for Removing Liquidity

1. Standard Liquidity Removal

Remove liquidity for a token pair to receive both tokens:

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. Get the pair address
    const pairAddress = await factory.getPair(tokenA, tokenB, fee);
    if (pairAddress === ethers.constants.AddressZero) {
        throw new Error("Pair does not exist");
    }
    
    // 2. Estimate the amount of tokens to receive
    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. Calculate minimum amounts (slippage protection)
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountAMin = amountA.mul(10000 - slippageBps).div(10000);
    const amountBMin = amountB.mul(10000 - slippageBps).div(10000);
    
    // 4. Approve LP tokens
    await ensureLPTokenApproval(pairAddress, ROUTER_ADDRESS, liquidity, signer);
    
    // 5. Remove liquidity
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.removeLiquidity(
        tokenA,
        tokenB,
        fee,
        liquidity,
        amountAMin,
        amountBMin,
        userAddress,
        deadline
    );
    
    console.log("Remove liquidity transaction submitted:", 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')
    };
}

// Helper function: ensure LP token approval
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("Approving LP tokens...");
        const approveTx = await pair.approve(spenderAddress, amount);
        await approveTx.wait();
        console.log("LP token approval complete");
    }
}

2. Removing JU Liquidity

Remove liquidity for a JU and ERC-20 token pair:

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. Get pair information
    const pairAddress = await factory.getPair(token, WJU_ADDRESS, fee);
    const pair = new ethers.Contract(pairAddress, pairABI, provider);
    
    // 2. Estimate the amounts to receive
    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. Calculate minimum amounts
    const slippageBps = Math.floor(slippagePercent * 100);
    const amountTokenMin = amountToken.mul(10000 - slippageBps).div(10000);
    const amountETHMin = amountETH.mul(10000 - slippageBps).div(10000);
    
    // 4. Approve LP tokens
    await ensureLPTokenApproval(pairAddress, ROUTER_ADDRESS, liquidity, signer);
    
    // 5. Remove liquidity
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    const tx = await router.removeLiquidityETH(
        token,
        fee,
        liquidity,
        amountTokenMin,
        amountETHMin,
        userAddress,
        deadline
    );
    
    console.log("Remove JU liquidity transaction submitted:", tx.hash);
    return tx;
}

Removing Liquidity Using Permit

Removal Without Pre-approval

Use the EIP-2612 Permit feature to remove liquidity directly with a signature:

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. Get the pair address
    const pairAddress = await factory.getPair(tokenA, tokenB, fee);
    const pair = new ethers.Contract(pairAddress, pairABI, provider);
    
    // 2. Calculate expected and minimum amounts
    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. Generate Permit signature
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    const permitSignature = await generatePermitSignature(
        pairAddress,
        userAddress,
        ROUTER_ADDRESS,
        liquidity,
        deadline,
        signer
    );
    
    // 4. Remove liquidity using Permit
    const tx = await router.removeLiquidityWithPermit(
        tokenA,
        tokenB,
        fee,
        liquidity,
        amountAMin,
        amountBMin,
        userAddress,
        deadline,
        false, // approveMax
        permitSignature.v,
        permitSignature.r,
        permitSignature.s
    );
    
    console.log("Remove liquidity with Permit transaction submitted:", tx.hash);
    return tx;
}

// Generate Permit signature
async function generatePermitSignature(
    tokenAddress,
    owner,
    spender,
    value,
    deadline,
    signer
) {
    const token = new ethers.Contract(tokenAddress, pairABI, provider);
    
    // Get the current nonce
    const nonce = await token.nonces(owner);
    
    // Get the chain ID
    const chainId = await signer.getChainId();
    
    // Build the EIP-712 domain
    const domain = {
        name: 'JAMM LPs',
        version: '1',
        chainId: chainId,
        verifyingContract: tokenAddress
    };
    
    // Build the message types
    const types = {
        Permit: [
            { name: 'owner', type: 'address' },
            { name: 'spender', type: 'address' },
            { name: 'value', type: 'uint256' },
            { name: 'nonce', type: 'uint256' },
            { name: 'deadline', type: 'uint256' }
        ]
    };
    
    // Build the message values
    const message = {
        owner: owner,
        spender: spender,
        value: value,
        nonce: nonce,
        deadline: deadline
    };
    
    // Generate the signature
    const signature = await signer._signTypedData(domain, types, message);
    const { v, r, s } = ethers.utils.splitSignature(signature);
    
    return { v, r, s, nonce, deadline };
}

Permit Removal for JU Liquidity

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);
    
    // Calculate expected amounts
    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);
    
    // Generate Permit signature
    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;
}

Removal for Fee-on-Transfer Tokens

Liquidity Removal for Fee-on-Transfer Tokens

For fee-on-transfer tokens, a special removal function is required:

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);
    
    // For fee-on-transfer tokens, it's hard to accurately estimate the output amounts
    // Set a higher slippage tolerance
    const adjustedSlippage = Math.max(slippagePercent, 5); // At least 5% slippage
    
    // Approve LP tokens
    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, // Set to 0, as it's difficult to estimate accurately
        0, // Set to 0, as it's difficult to estimate accurately
        userAddress,
        deadline
    );
    
    console.log("Remove liquidity for fee-on-transfer token transaction submitted:", tx.hash);
    return tx;
}

Partial Removal Strategies

Remove by Percentage

async function removePartialLiquidity(
    tokenA,
    tokenB,
    fee,
    percentage, // percentage to remove (0-100)
    userAddress,
    signer
) {
    const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
    
    // 1. Get the user's LP token balance
    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("No LP tokens to remove");
    }
    
    // 2. Calculate the amount to remove
    const liquidityToRemove = totalLPBalance.mul(percentage * 100).div(10000);
    
    console.log(`Removing ${percentage}% of liquidity`);
    console.log(`Total LP Tokens: ${ethers.utils.formatEther(totalLPBalance)}`);
    console.log(`Amount to remove: ${ethers.utils.formatEther(liquidityToRemove)}`);
    
    // 3. Execute removal
    const result = await removeLiquidity(
        tokenA,
        tokenB,
        fee,
        liquidityToRemove,
        1, // 1% slippage
        userAddress,
        signer
    );
    
    return result;
}

// Example usage
await removePartialLiquidity(
    TOKEN_A_ADDRESS,
    TOKEN_B_ADDRESS,
    100, // 1% fee rate
    25, // remove 25%
    userAddress,
    signer
);

Periodic Removal Strategy

class PeriodicLiquidityRemover {
    constructor(routerAddress, signer) {
        this.router = new ethers.Contract(routerAddress, routerABI, signer);
        this.signer = signer;
        this.schedules = [];
    }
    
    // Add a periodic removal schedule
    addSchedule(pairInfo, percentage, intervalDays) {
        this.schedules.push({
            ...pairInfo,
            percentage,
            intervalDays,
            lastRemoval: 0,
            nextRemoval: Date.now() + intervalDays * 24 * 60 * 60 * 1000
        });
    }
    
    // Check and execute scheduled removals
    async checkAndExecute() {
        const now = Date.now();
        
        for (const schedule of this.schedules) {
            if (now >= schedule.nextRemoval) {
                try {
                    console.log(`Executing periodic removal: ${schedule.tokenA}/${schedule.tokenB}`);
                    
                    await removePartialLiquidity(
                        schedule.tokenA,
                        schedule.tokenB,
                        schedule.fee,
                        schedule.percentage,
                        await this.signer.getAddress(),
                        this.signer
                    );
                    
                    // Update next removal time
                    schedule.lastRemoval = now;
                    schedule.nextRemoval = now + schedule.intervalDays * 24 * 60 * 60 * 1000;
                    
                } catch (error) {
                    console.error("Periodic removal failed:", error.message);
                }
            }
        }
    }
    
    // Start the scheduler
    startScheduler(checkIntervalMinutes = 60) {
        setInterval(() => {
            this.checkAndExecute();
        }, checkIntervalMinutes * 60 * 1000);
    }
}

Liquidity Analysis Tools

Return Calculation

async function calculateLiquidityReturns(
    pairAddress,
    userAddress,
    addedAmount,
    addedTimestamp
) {
    const pair = new ethers.Contract(pairAddress, pairABI, provider);
    
    // Get current LP balance
    const currentLP = await pair.balanceOf(userAddress);
    
    // Calculate current value
    const currentValue = await calculateLiquidityValue(pairAddress, currentLP, userAddress);
    
    // Calculate time difference
    const timeHeld = (Date.now() / 1000) - addedTimestamp; // seconds
    const daysHeld = timeHeld / (24 * 60 * 60);
    
    // Calculate returns
    const initialValue = addedAmount; // Assume initial value
    const currentTotalValue = currentValue.amount0.add(currentValue.amount1); // Simplified calculation
    const returns = currentTotalValue.sub(initialValue);
    const returnPercentage = returns.mul(10000).div(initialValue); // basis points
    
    // Annualized return
    const annualizedReturn = returnPercentage.mul(365).div(Math.floor(daysHeld));
    
    return {
        initialValue: initialValue,
        currentValue: currentTotalValue,
        returns: returns,
        returnPercentage: returnPercentage.toNumber() / 100, // percentage
        annualizedReturn: annualizedReturn.toNumber() / 100,
        daysHeld: daysHeld,
        currentLP: currentLP
    };
}

Impermanent Loss Calculation

function calculateImpermanentLoss(
    initialPriceRatio,
    currentPriceRatio,
    initialAmountA,
    initialAmountB
) {
    // Calculate the value if you had just held the tokens
    const holdValue = initialAmountA.add(
        initialAmountB.mul(currentPriceRatio).div(initialPriceRatio)
    );
    
    // Calculate the current value of the LP position (simplified)
    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));
    
    // Impermanent loss
    const impermanentLoss = holdValue.sub(lpValue);
    const impermanentLossPercentage = impermanentLoss.mul(10000).div(holdValue);
    
    return {
        holdValue: holdValue,
        lpValue: lpValue,
        impermanentLoss: impermanentLoss,
        impermanentLossPercentage: impermanentLossPercentage.toNumber() / 100
    };
}

// Simplified square root function
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;
}

Batch Removal Operations

Batch Remove Multiple Liquidity Positions

async function batchRemoveLiquidity(removeParams, signer) {
    const results = [];
    
    for (const params of removeParams) {
        try {
            console.log(`Removing liquidity: ${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
            });
            
            // Wait to avoid nonce conflicts
            await new Promise(resolve => setTimeout(resolve, 2000));
            
        } catch (error) {
            results.push({
                success: false,
                pair: `${params.tokenA}/${params.tokenB}`,
                error: error.message
            });
        }
    }
    
    return results;
}

Emergency Removal of All Liquidity

async function emergencyRemoveAllLiquidity(userAddress, signer) {
    const factory = new ethers.Contract(FACTORY_ADDRESS, factoryABI, provider);
    
    // Get all pairs
    const totalPairs = await factory.allPairsLength();
    const userLiquidities = [];
    
    // Check the user's liquidity in each pair
    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(`Failed to check pair ${i}:`, error.message);
        }
    }
    
    console.log(`Found ${userLiquidities.length} liquidity positions`);
    
    // Batch remove all liquidity positions
    if (userLiquidities.length > 0) {
        const results = await batchRemoveLiquidity(userLiquidities, signer);
        return results;
    }
    
    return [];
}

Best Practices

1. Choosing When to Remove

const removalTiming = {
    // Based on market conditions
    checkMarketConditions: (volatility, trend) => {
        if (volatility > 0.5 && trend === 'BEARISH') {
            return { action: 'REMOVE', reason: 'High volatility in a bearish market, removal recommended' };
        } else if (volatility < 0.2 && trend === 'BULLISH') {
            return { action: 'HOLD', reason: 'Low volatility in a bullish market, holding recommended' };
        }
        return { action: 'MONITOR', reason: 'Continue to monitor the market' };
    },
    
    // Based on returns
    checkReturns: (currentReturn, targetReturn, timeHeld) => {
        if (currentReturn >= targetReturn) {
            return { action: 'REMOVE', reason: 'Target return reached' };
        } else if (currentReturn < -0.1 && timeHeld > 30) { // Loss exceeds 10% and held for over 30 days
            return { action: 'CONSIDER_REMOVE', reason: 'Sustained losses, consider stop-loss' };
        }
        return { action: 'HOLD', reason: 'Removal conditions not met' };
    }
};

2. Gas Optimization Strategies

async function optimizeRemovalGas(removalParams, gasPrice) {
    // Prioritize using Permit to save gas
    const permitCapable = await checkPermitSupport(removalParams.pairAddress);
    
    if (permitCapable) {
        console.log("Using Permit to remove liquidity to save gas");
        return await removeLiquidityWithPermit(...removalParams);
    }
    
    // Batch approve to save gas
    const approvals = removalParams.map(params => 
        ensureLPTokenApproval(params.pairAddress, ROUTER_ADDRESS, params.liquidity, signer)
    );
    
    await Promise.all(approvals);
    
    // Sort by gas efficiency
    const sortedParams = removalParams.sort((a, b) => {
        // Prioritize large liquidity amounts
        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. Risk Management

const liquidityRiskManagement = {
    // Set a stop-loss point
    setStopLoss: (initialValue, stopLossPercentage = 10) => {
        return initialValue.mul(100 - stopLossPercentage).div(100);
    },
    
    // Set a take-profit point
    setTakeProfit: (initialValue, takeProfitPercentage = 50) => {
        return initialValue.mul(100 + takeProfitPercentage).div(100);
    },
    
    // Check if rebalancing is needed
    checkRebalance: (currentValue, stopLoss, takeProfit) => {
        if (currentValue.lte(stopLoss)) {
            return { action: 'STOP_LOSS', message: 'Stop-loss triggered' };
        } else if (currentValue.gte(takeProfit)) {
            return { action: 'TAKE_PROFIT', message: 'Take-profit triggered' };
        }
        return { action: 'HOLD', message: 'Continue holding' };
    }
};

Last updated