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:
A corresponding amount of LP tokens is burned.
You receive a proportional share of the two tokens in the pool.
You receive the accumulated trading fee earnings.
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